Merge pull request #102 from confio/search-min-max-height
Implement liveTx and min/max height search filters
This commit is contained in:
commit
2cbb6fed02
@ -5,6 +5,7 @@ import {
|
||||
Algorithm,
|
||||
Amount,
|
||||
ChainId,
|
||||
ConfirmedTransaction,
|
||||
isBlockInfoPending,
|
||||
isBlockInfoSucceeded,
|
||||
isConfirmedTransaction,
|
||||
@ -14,6 +15,7 @@ import {
|
||||
TokenTicker,
|
||||
TransactionId,
|
||||
TransactionState,
|
||||
UnsignedTransaction,
|
||||
} from "@iov/bcp";
|
||||
import { Random, Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto";
|
||||
import { Bech32, Encoding } from "@iov/encoding";
|
||||
@ -496,7 +498,7 @@ describe("CosmWasmConnection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration tests", () => {
|
||||
describe("searchTx", () => {
|
||||
it("can post and search for a transaction", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||
@ -587,6 +589,399 @@ describe("CosmWasmConnection", () => {
|
||||
connection.disconnect();
|
||||
});
|
||||
|
||||
it("can search by minHeight and maxHeight", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||
const profile = new UserProfile();
|
||||
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic));
|
||||
const sender = await profile.createIdentity(wallet.id, defaultChainId, faucet.path);
|
||||
const senderAddress = connection.codec.identityToAddress(sender);
|
||||
|
||||
const recipient = makeRandomAddress();
|
||||
const unsigned = await connection.withDefaultFee<SendTransaction>({
|
||||
kind: "bcp/send",
|
||||
chainId: defaultChainId,
|
||||
sender: senderAddress,
|
||||
recipient: recipient,
|
||||
memo: "My first payment",
|
||||
amount: {
|
||||
quantity: "75000",
|
||||
fractionalDigits: 6,
|
||||
tokenTicker: cosm,
|
||||
},
|
||||
});
|
||||
const nonce = await connection.getNonce({ address: senderAddress });
|
||||
const signed = await profile.signTransaction(sender, 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));
|
||||
assert(isBlockInfoSucceeded(blockInfo));
|
||||
const { height } = blockInfo;
|
||||
|
||||
// search by ID
|
||||
{
|
||||
const results = await connection.searchTx({ id: transactionId });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ id: transactionId, minHeight: height });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ id: transactionId, minHeight: height - 2 });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ id: transactionId, maxHeight: height });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ id: transactionId, maxHeight: height + 2 });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({
|
||||
id: transactionId,
|
||||
minHeight: height,
|
||||
maxHeight: height,
|
||||
});
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ id: transactionId, minHeight: height + 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ id: transactionId, maxHeight: height - 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({
|
||||
id: transactionId,
|
||||
minHeight: height + 1,
|
||||
maxHeight: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ id: transactionId, minHeight: 0, maxHeight: height - 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
|
||||
// search by recipient
|
||||
{
|
||||
const results = await connection.searchTx({ sentFromOrTo: recipient });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ sentFromOrTo: recipient, minHeight: height });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ sentFromOrTo: recipient, minHeight: height - 2 });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ sentFromOrTo: recipient, maxHeight: height });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ sentFromOrTo: recipient, maxHeight: height + 2 });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ sentFromOrTo: recipient, minHeight: height + 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ sentFromOrTo: recipient, maxHeight: height - 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({
|
||||
sentFromOrTo: recipient,
|
||||
minHeight: height,
|
||||
maxHeight: height,
|
||||
});
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ sentFromOrTo: recipient, minHeight: height + 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ sentFromOrTo: recipient, maxHeight: height - 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({
|
||||
sentFromOrTo: recipient,
|
||||
minHeight: height + 1,
|
||||
maxHeight: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({
|
||||
sentFromOrTo: recipient,
|
||||
minHeight: 0,
|
||||
maxHeight: height - 1,
|
||||
});
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
|
||||
// search by height
|
||||
{
|
||||
const results = await connection.searchTx({ height: height });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ height: height, minHeight: height });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ height: height, minHeight: height - 2 });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ height: height, maxHeight: height });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ height: height, maxHeight: height + 2 });
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ height: height, minHeight: height + 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ height: height, maxHeight: height - 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({
|
||||
height: height,
|
||||
minHeight: height,
|
||||
maxHeight: height,
|
||||
});
|
||||
expect(results.length).toEqual(1);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ height: height, minHeight: height + 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({ height: height, maxHeight: height - 1 });
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({
|
||||
height: height,
|
||||
minHeight: height + 1,
|
||||
maxHeight: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
{
|
||||
const results = await connection.searchTx({
|
||||
height: height,
|
||||
minHeight: 0,
|
||||
maxHeight: height - 1,
|
||||
});
|
||||
expect(results.length).toEqual(0);
|
||||
}
|
||||
|
||||
connection.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
describe("liveTx", () => {
|
||||
it("can listen to transactions by recipient address (transactions in history and updates)", done => {
|
||||
pendingWithoutWasmd();
|
||||
|
||||
(async () => {
|
||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||
|
||||
const profile = new UserProfile();
|
||||
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic));
|
||||
const sender = await profile.createIdentity(wallet.id, defaultChainId, faucet.path);
|
||||
|
||||
// send transactions
|
||||
|
||||
const recipientAddress = makeRandomAddress();
|
||||
const sendA = await connection.withDefaultFee<SendTransaction>({
|
||||
kind: "bcp/send",
|
||||
chainId: defaultChainId,
|
||||
senderPubkey: sender.pubkey,
|
||||
sender: connection.codec.identityToAddress(sender),
|
||||
recipient: recipientAddress,
|
||||
amount: defaultAmount,
|
||||
memo: `liveTx() test A ${Math.random()}`,
|
||||
});
|
||||
|
||||
const sendB = await connection.withDefaultFee<SendTransaction>({
|
||||
kind: "bcp/send",
|
||||
chainId: defaultChainId,
|
||||
senderPubkey: sender.pubkey,
|
||||
sender: connection.codec.identityToAddress(sender),
|
||||
recipient: recipientAddress,
|
||||
amount: defaultAmount,
|
||||
memo: `liveTx() test B ${Math.random()}`,
|
||||
});
|
||||
|
||||
const sendC = await connection.withDefaultFee<SendTransaction>({
|
||||
kind: "bcp/send",
|
||||
chainId: defaultChainId,
|
||||
senderPubkey: sender.pubkey,
|
||||
sender: connection.codec.identityToAddress(sender),
|
||||
recipient: recipientAddress,
|
||||
amount: defaultAmount,
|
||||
memo: `liveTx() test C ${Math.random()}`,
|
||||
});
|
||||
|
||||
const [nonceA, nonceB, nonceC] = await connection.getNonces({ pubkey: sender.pubkey }, 3);
|
||||
const signedA = await profile.signTransaction(sender, sendA, connection.codec, nonceA);
|
||||
const signedB = await profile.signTransaction(sender, sendB, connection.codec, nonceB);
|
||||
const signedC = await profile.signTransaction(sender, sendC, connection.codec, nonceC);
|
||||
const bytesToPostA = connection.codec.bytesToPost(signedA);
|
||||
const bytesToPostB = connection.codec.bytesToPost(signedB);
|
||||
const bytesToPostC = connection.codec.bytesToPost(signedC);
|
||||
|
||||
// Post A and B. Unfortunately the REST server API does not support sending them in parallel because the sequence check fails.
|
||||
const postResultA = await connection.postTx(bytesToPostA);
|
||||
await postResultA.blockInfo.waitFor(info => !isBlockInfoPending(info));
|
||||
const postResultB = await connection.postTx(bytesToPostB);
|
||||
await postResultB.blockInfo.waitFor(info => !isBlockInfoPending(info));
|
||||
|
||||
// setup listener after A and B are in block
|
||||
const events = new Array<ConfirmedTransaction<UnsignedTransaction>>();
|
||||
const subscription = connection.liveTx({ sentFromOrTo: recipientAddress }).subscribe({
|
||||
next: event => {
|
||||
assert(isConfirmedTransaction(event), "Confirmed transaction expected");
|
||||
events.push(event);
|
||||
|
||||
assert(isSendTransaction(event.transaction), "Unexpected transaction type");
|
||||
expect(event.transaction.recipient).toEqual(recipientAddress);
|
||||
|
||||
if (events.length === 3) {
|
||||
expect(events[1].height).toEqual(events[0].height + 1);
|
||||
expect(events[2].height).toBeGreaterThan(events[1].height);
|
||||
|
||||
subscription.unsubscribe();
|
||||
connection.disconnect();
|
||||
done();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Post C
|
||||
await connection.postTx(bytesToPostC);
|
||||
})().catch(done.fail);
|
||||
});
|
||||
|
||||
it("can listen to transactions by ID (transaction in history)", done => {
|
||||
pendingWithoutWasmd();
|
||||
|
||||
(async () => {
|
||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||
|
||||
const profile = new UserProfile();
|
||||
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic));
|
||||
const sender = await profile.createIdentity(wallet.id, defaultChainId, faucet.path);
|
||||
|
||||
const recipientAddress = makeRandomAddress();
|
||||
const send = await connection.withDefaultFee<SendTransaction>({
|
||||
kind: "bcp/send",
|
||||
chainId: defaultChainId,
|
||||
senderPubkey: sender.pubkey,
|
||||
sender: connection.codec.identityToAddress(sender),
|
||||
recipient: recipientAddress,
|
||||
amount: defaultAmount,
|
||||
memo: `liveTx() test ${Math.random()}`,
|
||||
});
|
||||
|
||||
const nonce = await connection.getNonce({ pubkey: sender.pubkey });
|
||||
const signed = await profile.signTransaction(sender, send, connection.codec, nonce);
|
||||
const bytesToPost = connection.codec.bytesToPost(signed);
|
||||
|
||||
const postResult = await connection.postTx(bytesToPost);
|
||||
const transactionId = postResult.transactionId;
|
||||
|
||||
// Wait for a block
|
||||
await postResult.blockInfo.waitFor(info => !isBlockInfoPending(info));
|
||||
|
||||
// setup listener after transaction is in block
|
||||
const events = new Array<ConfirmedTransaction<UnsignedTransaction>>();
|
||||
const subscription = connection.liveTx({ id: transactionId }).subscribe({
|
||||
next: event => {
|
||||
assert(isConfirmedTransaction(event), "Confirmed transaction expected");
|
||||
events.push(event);
|
||||
|
||||
assert(isSendTransaction(event.transaction), "Unexpected transaction type");
|
||||
expect(event.transaction.recipient).toEqual(recipientAddress);
|
||||
expect(event.transactionId).toEqual(transactionId);
|
||||
|
||||
subscription.unsubscribe();
|
||||
connection.disconnect();
|
||||
done();
|
||||
},
|
||||
});
|
||||
})().catch(done.fail);
|
||||
});
|
||||
|
||||
it("can listen to transactions by ID (transaction in updates)", done => {
|
||||
pendingWithoutWasmd();
|
||||
|
||||
(async () => {
|
||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||
|
||||
const profile = new UserProfile();
|
||||
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic));
|
||||
const sender = await profile.createIdentity(wallet.id, defaultChainId, faucet.path);
|
||||
|
||||
// send transactions
|
||||
|
||||
const recipientAddress = makeRandomAddress();
|
||||
const send = await connection.withDefaultFee<SendTransaction>({
|
||||
kind: "bcp/send",
|
||||
chainId: defaultChainId,
|
||||
senderPubkey: sender.pubkey,
|
||||
sender: connection.codec.identityToAddress(sender),
|
||||
recipient: recipientAddress,
|
||||
amount: defaultAmount,
|
||||
memo: `liveTx() test ${Math.random()}`,
|
||||
});
|
||||
|
||||
const nonce = await connection.getNonce({ pubkey: sender.pubkey });
|
||||
const signed = await profile.signTransaction(sender, send, connection.codec, nonce);
|
||||
const bytesToPost = connection.codec.bytesToPost(signed);
|
||||
|
||||
const postResult = await connection.postTx(bytesToPost);
|
||||
const transactionId = postResult.transactionId;
|
||||
|
||||
// setup listener before transaction is in block
|
||||
const events = new Array<ConfirmedTransaction<UnsignedTransaction>>();
|
||||
const subscription = connection.liveTx({ id: transactionId }).subscribe({
|
||||
next: event => {
|
||||
assert(isConfirmedTransaction(event), "Confirmed transaction expected");
|
||||
events.push(event);
|
||||
|
||||
assert(isSendTransaction(event.transaction), "Unexpected transaction type");
|
||||
expect(event.transaction.recipient).toEqual(recipientAddress);
|
||||
expect(event.transactionId).toEqual(transactionId);
|
||||
|
||||
subscription.unsubscribe();
|
||||
connection.disconnect();
|
||||
done();
|
||||
},
|
||||
});
|
||||
})().catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration tests", () => {
|
||||
it("can send ERC20 tokens", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||
|
||||
@ -37,7 +37,7 @@ import {
|
||||
UnsignedTransaction,
|
||||
} from "@iov/bcp";
|
||||
import { Encoding, Uint53 } from "@iov/encoding";
|
||||
import { DefaultValueProducer, ValueAndUpdates } from "@iov/stream";
|
||||
import { concat, DefaultValueProducer, ValueAndUpdates } from "@iov/stream";
|
||||
import BN from "bn.js";
|
||||
import equal from "fast-deep-equal";
|
||||
import { ReadonlyDate } from "readonly-date";
|
||||
@ -304,8 +304,8 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
public async searchTx({
|
||||
height,
|
||||
id,
|
||||
maxHeight,
|
||||
minHeight,
|
||||
maxHeight: maxHeightOptional,
|
||||
minHeight: minHeightOptional,
|
||||
sentFromOrTo,
|
||||
signedBy,
|
||||
tags,
|
||||
@ -314,30 +314,38 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
throw new Error("Transaction query by signedBy or tags not yet supported");
|
||||
}
|
||||
|
||||
if ([maxHeight, minHeight].some(isDefined)) {
|
||||
throw new Error(
|
||||
"Transaction query by minHeight/maxHeight not yet supported. This is due to missing flexibility of the Gaia REST API, see https://github.com/cosmos/gaia/issues/75",
|
||||
);
|
||||
}
|
||||
|
||||
if ([id, height, sentFromOrTo].filter(isDefined).length !== 1) {
|
||||
throw new Error(
|
||||
"Transaction query by id, height and sentFromOrTo is mutually exclusive. Exactly one must be set.",
|
||||
);
|
||||
}
|
||||
|
||||
const minHeight = minHeightOptional || 0;
|
||||
const maxHeight = maxHeightOptional || Number.MAX_SAFE_INTEGER;
|
||||
|
||||
if (maxHeight < minHeight) return []; // optional optimization
|
||||
|
||||
let txs: readonly TxsResponse[];
|
||||
if (id) {
|
||||
txs = await this.cosmWasmClient.searchTx({ id: id });
|
||||
} else if (height) {
|
||||
if (height < minHeight) return []; // optional optimization
|
||||
if (height > maxHeight) return []; // optional optimization
|
||||
txs = await this.cosmWasmClient.searchTx({ height: height });
|
||||
} else if (sentFromOrTo) {
|
||||
// TODO: pass minHeight/maxHeight to server once we have
|
||||
// https://github.com/cosmwasm/wasmd/issues/73
|
||||
txs = await this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo });
|
||||
} else {
|
||||
throw new Error("Unsupported query");
|
||||
}
|
||||
|
||||
return txs.map(tx => this.parseAndPopulateTxResponseUnsigned(tx));
|
||||
const filtered = txs.filter(tx => {
|
||||
const txHeight = parseInt(tx.height, 10);
|
||||
return txHeight >= minHeight && txHeight <= maxHeight;
|
||||
});
|
||||
|
||||
return filtered.map(tx => this.parseAndPopulateTxResponseUnsigned(tx));
|
||||
}
|
||||
|
||||
public listenTx(
|
||||
@ -347,9 +355,55 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
}
|
||||
|
||||
public liveTx(
|
||||
_query: TransactionQuery,
|
||||
query: TransactionQuery,
|
||||
): Stream<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction> {
|
||||
throw new Error("not implemented");
|
||||
if ([query.height, query.signedBy, query.tags].some(isDefined)) {
|
||||
throw new Error("Transaction query by height, signedBy or tags not yet supported");
|
||||
}
|
||||
|
||||
if (query.id) {
|
||||
if (query.minHeight || query.maxHeight) {
|
||||
throw new Error("Query by minHeight/maxHeight not supported together with ID");
|
||||
}
|
||||
|
||||
// concat never() because we want non-completing streams consistently
|
||||
return concat(this.waitForTransaction(query.id), Stream.never());
|
||||
} else if (query.sentFromOrTo) {
|
||||
let pollInternal: NodeJS.Timeout | undefined;
|
||||
const producer: Producer<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction> = {
|
||||
start: async listener => {
|
||||
let minHeight = query.minHeight || 0;
|
||||
const maxHeight = query.maxHeight || Number.MAX_SAFE_INTEGER;
|
||||
|
||||
const poll = async (): Promise<void> => {
|
||||
const result = await this.searchTx({
|
||||
sentFromOrTo: query.sentFromOrTo,
|
||||
minHeight: minHeight,
|
||||
maxHeight: maxHeight,
|
||||
});
|
||||
for (const item of result) {
|
||||
listener.next(item);
|
||||
if (item.height >= minHeight) {
|
||||
// we assume we got all matching transactions from block `item.height` now
|
||||
minHeight = item.height + 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await poll();
|
||||
pollInternal = setInterval(poll, defaultPollInterval);
|
||||
},
|
||||
stop: () => {
|
||||
if (pollInternal) {
|
||||
clearInterval(pollInternal);
|
||||
pollInternal = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
return Stream.create(producer);
|
||||
} else {
|
||||
throw new Error("Unsupported query.");
|
||||
}
|
||||
}
|
||||
|
||||
public async getFeeQuote(tx: UnsignedTransaction): Promise<Fee> {
|
||||
@ -425,4 +479,43 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
this.erc20Tokens,
|
||||
);
|
||||
}
|
||||
|
||||
private waitForTransaction(
|
||||
id: TransactionId,
|
||||
): Stream<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction> {
|
||||
let pollInternal: NodeJS.Timeout | undefined;
|
||||
const producer: Producer<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction> = {
|
||||
start: listener => {
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const results = await this.searchTx({ id: id });
|
||||
switch (results.length) {
|
||||
case 0:
|
||||
// okay, we'll try again
|
||||
break;
|
||||
case 1:
|
||||
listener.next(results[0]);
|
||||
listener.complete();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Got unexpected number of search results: ${results.length}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (pollInternal) {
|
||||
clearTimeout(pollInternal);
|
||||
pollInternal = undefined;
|
||||
}
|
||||
listener.error(error);
|
||||
}
|
||||
}, defaultPollInterval);
|
||||
},
|
||||
stop: () => {
|
||||
if (pollInternal) {
|
||||
clearTimeout(pollInternal);
|
||||
pollInternal = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
return Stream.create(producer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,7 +230,8 @@ describe("encode", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("builds a send transaction without fee", () => {
|
||||
it("throws for a send transaction without fee", () => {
|
||||
// This will be rejected by the REST server. Better throw early to avoid hard to debug errors.
|
||||
const tx = {
|
||||
kind: "bcp/send",
|
||||
chainId: defaultChainId,
|
||||
@ -239,32 +240,7 @@ describe("encode", () => {
|
||||
recipient: defaultRecipient,
|
||||
memo: defaultMemo,
|
||||
};
|
||||
expect(buildUnsignedTx(tx, defaultTokens)).toEqual({
|
||||
type: "cosmos-sdk/StdTx",
|
||||
value: {
|
||||
msg: [
|
||||
{
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r",
|
||||
to_address: "cosmos1z7g5w84ynmjyg0kqpahdjqpj7yq34v3suckp0e",
|
||||
amount: [
|
||||
{
|
||||
denom: "uatom",
|
||||
amount: "11657995",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
signatures: [],
|
||||
memo: defaultMemo,
|
||||
fee: {
|
||||
amount: [],
|
||||
gas: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(() => buildUnsignedTx(tx, defaultTokens)).toThrowError(/transaction fee must be set/i);
|
||||
});
|
||||
|
||||
it("builds a send transaction with fee", () => {
|
||||
|
||||
@ -91,6 +91,8 @@ export function buildUnsignedTx(
|
||||
const matchingBankToken = bankTokens.find(t => t.ticker === tx.amount.tokenTicker);
|
||||
const matchingErc20Token = erc20Tokens.find(t => t.ticker === tx.amount.tokenTicker);
|
||||
|
||||
if (!tx.fee) throw new Error("Transaction fee must be set");
|
||||
|
||||
if (matchingBankToken) {
|
||||
return {
|
||||
type: "cosmos-sdk/StdTx",
|
||||
@ -107,12 +109,7 @@ export function buildUnsignedTx(
|
||||
],
|
||||
memo: tx.memo || "",
|
||||
signatures: [],
|
||||
fee: tx.fee
|
||||
? encodeFee(tx.fee, bankTokens)
|
||||
: {
|
||||
amount: [],
|
||||
gas: "",
|
||||
},
|
||||
fee: encodeFee(tx.fee, bankTokens),
|
||||
},
|
||||
};
|
||||
} else if (matchingErc20Token) {
|
||||
@ -137,12 +134,7 @@ export function buildUnsignedTx(
|
||||
],
|
||||
memo: tx.memo || "",
|
||||
signatures: [],
|
||||
fee: tx.fee
|
||||
? encodeFee(tx.fee, bankTokens)
|
||||
: {
|
||||
amount: [],
|
||||
gas: "",
|
||||
},
|
||||
fee: encodeFee(tx.fee, bankTokens),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
|
||||
7
packages/bcp/types/cosmwasmconnection.d.ts
vendored
7
packages/bcp/types/cosmwasmconnection.d.ts
vendored
@ -74,16 +74,17 @@ export declare class CosmWasmConnection implements BlockchainConnection {
|
||||
searchTx({
|
||||
height,
|
||||
id,
|
||||
maxHeight,
|
||||
minHeight,
|
||||
maxHeight: maxHeightOptional,
|
||||
minHeight: minHeightOptional,
|
||||
sentFromOrTo,
|
||||
signedBy,
|
||||
tags,
|
||||
}: TransactionQuery): Promise<readonly (ConfirmedTransaction<UnsignedTransaction> | FailedTransaction)[]>;
|
||||
listenTx(_query: TransactionQuery): Stream<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction>;
|
||||
liveTx(_query: TransactionQuery): Stream<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction>;
|
||||
liveTx(query: TransactionQuery): Stream<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction>;
|
||||
getFeeQuote(tx: UnsignedTransaction): Promise<Fee>;
|
||||
withDefaultFee<T extends UnsignedTransaction>(tx: T): Promise<T>;
|
||||
private parseAndPopulateTxResponseUnsigned;
|
||||
private parseAndPopulateTxResponseSigned;
|
||||
private waitForTransaction;
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { Sha256 } from "@iov/crypto";
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { Log, parseLogs } from "./logs";
|
||||
import { BlockResponse, RestClient, TxsResponse } from "./restclient";
|
||||
import { BlockResponse, BroadcastMode, RestClient, TxsResponse } from "./restclient";
|
||||
import { CosmosSdkAccount, CosmosSdkTx } from "./types";
|
||||
|
||||
export interface GetNonceResult {
|
||||
@ -46,8 +46,8 @@ function isSearchBySentFromOrToQuery(query: SearchTxQuery): query is SearchBySen
|
||||
export class CosmWasmClient {
|
||||
protected readonly restClient: RestClient;
|
||||
|
||||
public constructor(url: string) {
|
||||
this.restClient = new RestClient(url);
|
||||
public constructor(url: string, broadcastMode = BroadcastMode.Block) {
|
||||
this.restClient = new RestClient(url, broadcastMode);
|
||||
}
|
||||
|
||||
public async chainId(): Promise<string> {
|
||||
@ -136,7 +136,7 @@ export class CosmWasmClient {
|
||||
}
|
||||
|
||||
return {
|
||||
logs: parseLogs(result.logs) || [],
|
||||
logs: result.logs ? parseLogs(result.logs) : [],
|
||||
rawLog: result.raw_log || "",
|
||||
transactionHash: result.txhash,
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@ export { logs, types };
|
||||
export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./address";
|
||||
export { unmarshalTx } from "./decoding";
|
||||
export { makeSignBytes, marshalTx } from "./encoding";
|
||||
export { RestClient, TxsResponse } from "./restclient";
|
||||
export { BroadcastMode, RestClient, TxsResponse } from "./restclient";
|
||||
export {
|
||||
CosmWasmClient,
|
||||
GetNonceResult,
|
||||
|
||||
@ -152,7 +152,19 @@ type RestClientResponse =
|
||||
| WasmResponse<string>
|
||||
| WasmResponse<GetCodeResult>;
|
||||
|
||||
type BroadcastMode = "block" | "sync" | "async";
|
||||
/**
|
||||
* The mode used to send transaction
|
||||
*
|
||||
* @see https://cosmos.network/rpc/#/Transactions/post_txs
|
||||
*/
|
||||
export enum BroadcastMode {
|
||||
/** Return after tx commit */
|
||||
Block = "block",
|
||||
/** Return afer CheckTx */
|
||||
Sync = "sync",
|
||||
/** Return right away */
|
||||
Async = "async",
|
||||
}
|
||||
|
||||
function isWasmError<T>(resp: WasmResponse<T>): resp is WasmError {
|
||||
return (resp as WasmError).error !== undefined;
|
||||
@ -194,11 +206,9 @@ function parseAxios500error(err: AxiosError): never {
|
||||
|
||||
export class RestClient {
|
||||
private readonly client: AxiosInstance;
|
||||
// From https://cosmos.network/rpc/#/ICS0/post_txs
|
||||
// The supported broadcast modes include "block"(return after tx commit), "sync"(return afer CheckTx) and "async"(return right away).
|
||||
private readonly mode: BroadcastMode;
|
||||
|
||||
public constructor(url: string, mode: BroadcastMode = "block") {
|
||||
public constructor(url: string, mode = BroadcastMode.Block) {
|
||||
const headers = {
|
||||
post: { "Content-Type": "application/json" },
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import { Encoding } from "@iov/encoding";
|
||||
import { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient";
|
||||
import { makeSignBytes, marshalTx } from "./encoding";
|
||||
import { findAttribute, Log } from "./logs";
|
||||
import { BroadcastMode } from "./restclient";
|
||||
import {
|
||||
Coin,
|
||||
CosmosSdkAccount,
|
||||
@ -63,8 +64,9 @@ export class SigningCosmWasmClient extends CosmWasmClient {
|
||||
senderAddress: string,
|
||||
signCallback: SigningCallback,
|
||||
customFees?: Partial<FeeTable>,
|
||||
broadcastMode = BroadcastMode.Block,
|
||||
) {
|
||||
super(url);
|
||||
super(url, broadcastMode);
|
||||
this.senderAddress = senderAddress;
|
||||
this.signCallback = signCallback;
|
||||
this.fees = { ...defaultFees, ...(customFees || {}) };
|
||||
|
||||
4
packages/sdk/types/cosmwasmclient.d.ts
vendored
4
packages/sdk/types/cosmwasmclient.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
import { Log } from "./logs";
|
||||
import { BlockResponse, RestClient, TxsResponse } from "./restclient";
|
||||
import { BlockResponse, BroadcastMode, RestClient, TxsResponse } from "./restclient";
|
||||
import { CosmosSdkAccount, CosmosSdkTx } from "./types";
|
||||
export interface GetNonceResult {
|
||||
readonly accountNumber: number;
|
||||
@ -23,7 +23,7 @@ export interface SearchBySentFromOrToQuery {
|
||||
export declare type SearchTxQuery = SearchByIdQuery | SearchByHeightQuery | SearchBySentFromOrToQuery;
|
||||
export declare class CosmWasmClient {
|
||||
protected readonly restClient: RestClient;
|
||||
constructor(url: string);
|
||||
constructor(url: string, broadcastMode?: BroadcastMode);
|
||||
chainId(): Promise<string>;
|
||||
/**
|
||||
* Returns a 32 byte upper-case hex transaction hash (typically used as the transaction ID)
|
||||
|
||||
2
packages/sdk/types/index.d.ts
vendored
2
packages/sdk/types/index.d.ts
vendored
@ -4,7 +4,7 @@ export { logs, types };
|
||||
export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./address";
|
||||
export { unmarshalTx } from "./decoding";
|
||||
export { makeSignBytes, marshalTx } from "./encoding";
|
||||
export { RestClient, TxsResponse } from "./restclient";
|
||||
export { BroadcastMode, RestClient, TxsResponse } from "./restclient";
|
||||
export {
|
||||
CosmWasmClient,
|
||||
GetNonceResult,
|
||||
|
||||
14
packages/sdk/types/restclient.d.ts
vendored
14
packages/sdk/types/restclient.d.ts
vendored
@ -105,7 +105,19 @@ declare type RestClientResponse =
|
||||
| EncodeTxResponse
|
||||
| WasmResponse<string>
|
||||
| WasmResponse<GetCodeResult>;
|
||||
declare type BroadcastMode = "block" | "sync" | "async";
|
||||
/**
|
||||
* The mode used to send transaction
|
||||
*
|
||||
* @see https://cosmos.network/rpc/#/Transactions/post_txs
|
||||
*/
|
||||
export declare enum BroadcastMode {
|
||||
/** Return after tx commit */
|
||||
Block = "block",
|
||||
/** Return afer CheckTx */
|
||||
Sync = "sync",
|
||||
/** Return right away */
|
||||
Async = "async",
|
||||
}
|
||||
export declare class RestClient {
|
||||
private readonly client;
|
||||
private readonly mode;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient";
|
||||
import { Log } from "./logs";
|
||||
import { BroadcastMode } from "./restclient";
|
||||
import { Coin, CosmosSdkAccount, StdFee, StdSignature } from "./types";
|
||||
export interface SigningCallback {
|
||||
(signBytes: Uint8Array): Promise<StdSignature>;
|
||||
@ -22,6 +23,7 @@ export declare class SigningCosmWasmClient extends CosmWasmClient {
|
||||
senderAddress: string,
|
||||
signCallback: SigningCallback,
|
||||
customFees?: Partial<FeeTable>,
|
||||
broadcastMode?: BroadcastMode,
|
||||
);
|
||||
getNonce(address?: string): Promise<GetNonceResult>;
|
||||
getAccount(address?: string): Promise<CosmosSdkAccount | undefined>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user