Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Cosmos connection #1285

Merged
merged 33 commits into from
Dec 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0a5300d
Add basic REST client to @iov/cosmos
willclarktech Oct 17, 2019
fcf2eb3
Implement and test some basic GET methods in Cosmos connection
willclarktech Oct 17, 2019
ddb16b9
Add Cosmos HdPaths method to @iov/keycontrol
willclarktech Oct 22, 2019
7cc8dc5
Add basic post/get tx methods to Cosmos REST client
willclarktech Oct 23, 2019
eed9a25
Add parseTxsResponse helper function to Cosmos decode
willclarktech Oct 24, 2019
073f900
Implement basic Cosmos connection withDefaultFee method
willclarktech Oct 23, 2019
ed104fb
Implement Cosmos connection getNonce(s) methods
willclarktech Oct 23, 2019
efaab39
Add basic get/postTx methods to Cosmos connection
willclarktech Oct 23, 2019
7c8a381
Add basic get/postTx integration test for Cosmos connection
willclarktech Oct 23, 2019
888c8b1
Implement basic getFeeQuote in Cosmos connection
willclarktech Oct 24, 2019
96edeed
Add expections for Cosmos connection post/getTx test
willclarktech Oct 24, 2019
6f3002b
Remove unnecessary sleep from Cosmos connection test
willclarktech Oct 24, 2019
b7224b4
Add @iov/stream dependency to @iov/cosmos
willclarktech Oct 24, 2019
1eac4eb
Add axios dependency to @iov/cosmos
willclarktech Oct 24, 2019
afcd27e
Add readonly-date dependency to @iov/cosmos
willclarktech Oct 24, 2019
de4d7e6
Add @iov/keycontrol dev dependency to @iov/cosmos
willclarktech Oct 24, 2019
2c58848
Export connection and connector in @iov/cosmos index
willclarktech Oct 24, 2019
2947ea7
Add txs method to Cosmos REST client
willclarktech Oct 24, 2019
de2e8ba
Add REST server listen address to Cosmos start script
willclarktech Oct 24, 2019
35f165f
Handle null responses in Cosmos REST client get method
willclarktech Oct 24, 2019
c754d30
Add basic implementation for Cosmos connection searchTx method
willclarktech Oct 24, 2019
baa8692
Add post/searchTx integration test for Cosmos connection
willclarktech Oct 24, 2019
539b6b6
Add optional height argument to Cosmos REST client authAccounts method
willclarktech Oct 29, 2019
c039f03
Check for nonce in Cosmos connection getTx test
willclarktech Oct 29, 2019
d51124b
Return nonce in Cosmos connection get/searchTx
willclarktech Oct 29, 2019
3a0a382
Make Cosmos connection nonce population DRYer
willclarktech Oct 29, 2019
30b4077
Update lint settings for Cosmos connection
willclarktech Oct 29, 2019
93b9c04
Increase sleep time for gaiad REST server start
willclarktech Oct 29, 2019
1a722e3
Disable web security in Cosmos Chrome tests
willclarktech Oct 30, 2019
a2919ff
Make Cosmos REST client DRYer
willclarktech Oct 30, 2019
5ea8592
Make Cosmos REST client more uniform
willclarktech Oct 30, 2019
50579a0
Use ATOM/uatom in Cosmos test setup instead of vatom
willclarktech Nov 6, 2019
0af441d
Specify supported tokens in Cosmos connection
willclarktech Nov 6, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/iov-cosmos/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,12 @@ module.exports = function(config) {

// Keep brower open for debugging. This is overridden by yarn scripts
singleRun: false,

customLaunchers: {
ChromeHeadlessInsecure: {
base: "ChromeHeadless",
flags: ["--disable-web-security"],
},
},
});
};
8 changes: 7 additions & 1 deletion packages/iov-cosmos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"test-node": "node jasmine-testrunner.js",
"test-edge": "yarn pack-web && karma start --single-run --browsers Edge",
"test-firefox": "yarn pack-web && karma start --single-run --browsers Firefox",
"test-chrome": "yarn pack-web && karma start --single-run --browsers ChromeHeadless",
"test-chrome": "yarn pack-web && karma start --single-run --browsers ChromeHeadlessInsecure",
"test-safari": "yarn pack-web && karma start --single-run --browsers Safari",
"test": "yarn build-or-skip && yarn test-node",
"pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js"
Expand All @@ -41,7 +41,13 @@
"@iov/bcp": "^1.0.0",
"@iov/crypto": "^1.0.0",
"@iov/encoding": "^1.0.0",
"@iov/stream": "^1.0.0",
"@tendermint/amino-js": "^0.6.2",
"axios": "^0.19.0",
"readonly-date": "^1.0.0",
"xstream": "^11.11.0"
},
"devDependencies": {
"@iov/keycontrol": "^1.0.0"
}
}
291 changes: 291 additions & 0 deletions packages/iov-cosmos/src/cosmosconnection.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import {
Address,
Algorithm,
ChainId,
isFailedTransaction,
isSendTransaction,
PubkeyBytes,
SendTransaction,
TokenTicker,
WithCreator,
} from "@iov/bcp";
import { Secp256k1 } from "@iov/crypto";
import { Encoding } from "@iov/encoding";
import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol";

import { cosmosCodec } from "./cosmoscodec";
import { CosmosConnection } from "./cosmosconnection";

const { fromBase64, toHex } = Encoding;

function pendingWithoutCosmos(): void {
if (!process.env.COSMOS_ENABLED) {
return pending("Set COSMOS_ENABLED to enable Cosmos node-based tests");
}
}

describe("CosmosConnection", () => {
const atom = "ATOM" as TokenTicker;
const httpUrl = "http://localhost:1317";
const defaultChainId = "testing" as ChainId;
const defaultEmptyAddress = "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address;
const defaultAddress = "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" as Address;
const defaultPubkey = {
algo: Algorithm.Secp256k1,
data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ") as PubkeyBytes,
};
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;

describe("establish", () => {
it("can connect to Cosmos via http", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
expect(connection).toBeTruthy();
connection.disconnect();
});
});

describe("chainId", () => {
it("displays the chain ID", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const chainId = connection.chainId();
expect(chainId).toEqual(defaultChainId);
connection.disconnect();
});
});

describe("height", () => {
it("displays the current height", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const height = await connection.height();
expect(height).toBeGreaterThan(0);
connection.disconnect();
});
});

describe("getAccount", () => {
it("gets an empty account by address", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const account = await connection.getAccount({ address: defaultEmptyAddress });
expect(account).toBeUndefined();
connection.disconnect();
});

it("gets an account by address", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const account = await connection.getAccount({ address: defaultAddress });
if (account === undefined) {
throw new Error("Expected account not to be undefined");
}
expect(account.address).toEqual(defaultAddress);
expect(account.pubkey).toEqual(defaultPubkey);
// Unsupported coins are filtered out
expect(account.balance.length).toEqual(1);
connection.disconnect();
});

it("gets an account by pubkey", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const account = await connection.getAccount({ pubkey: defaultPubkey });
if (account === undefined) {
throw new Error("Expected account not to be undefined");
}
expect(account.address).toEqual(defaultAddress);
expect(account.pubkey).toEqual({
algo: Algorithm.Secp256k1,
data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"),
});
// Unsupported coins are filtered out
expect(account.balance.length).toEqual(1);
connection.disconnect();
});
});

describe("integration tests", () => {
it("can post and get a transaction", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic));
const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath);
const faucetAddress = cosmosCodec.identityToAddress(faucet);

const unsigned = await connection.withDefaultFee<SendTransaction & WithCreator>({
kind: "bcp/send",
creator: faucet,
sender: faucetAddress,
recipient: defaultRecipient,
memo: "My first payment",
amount: {
quantity: "75000",
fractionalDigits: 6,
tokenTicker: atom,
},
});
const nonce = await connection.getNonce({ address: faucetAddress });
const signed = await profile.signTransaction(unsigned, cosmosCodec, nonce);
const postableBytes = cosmosCodec.bytesToPost(signed);
const { transactionId } = await connection.postTx(postableBytes);

const getResponse = await connection.getTx(transactionId);
expect(getResponse).toBeTruthy();
expect(getResponse.transactionId).toEqual(transactionId);
if (isFailedTransaction(getResponse)) {
throw new Error("Expected transaction to succeed");
}
expect(getResponse.log).toMatch(/success/i);
const { transaction, primarySignature, otherSignatures } = 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.creator.chainId).toEqual(unsigned.creator.chainId);
expect(transaction.creator.pubkey.algo).toEqual(unsigned.creator.pubkey.algo);
expect(toHex(transaction.creator.pubkey.data)).toEqual(
toHex(Secp256k1.compressPubkey(unsigned.creator.pubkey.data)),
);

expect(primarySignature.nonce).toEqual(signed.primarySignature.nonce);
expect(primarySignature.pubkey.algo).toEqual(signed.primarySignature.pubkey.algo);
expect(toHex(primarySignature.pubkey.data)).toEqual(
toHex(Secp256k1.compressPubkey(signed.primarySignature.pubkey.data)),
);
expect(toHex(primarySignature.signature)).toEqual(
toHex(Secp256k1.trimRecoveryByte(signed.primarySignature.signature)),
);
expect(otherSignatures).toEqual(signed.otherSignatures);

connection.disconnect();
});

it("can post and search for a transaction", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic));
const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath);
const faucetAddress = cosmosCodec.identityToAddress(faucet);

const unsigned = await connection.withDefaultFee<SendTransaction & WithCreator>({
kind: "bcp/send",
creator: faucet,
sender: faucetAddress,
recipient: defaultRecipient,
memo: "My first payment",
amount: {
quantity: "75000",
fractionalDigits: 6,
tokenTicker: atom,
},
});
const nonce = await connection.getNonce({ address: faucetAddress });
const signed = await profile.signTransaction(unsigned, cosmosCodec, nonce);
const postableBytes = cosmosCodec.bytesToPost(signed);
const { transactionId } = await connection.postTx(postableBytes);

// search by id

const idSearchResponse = await connection.searchTx({ id: transactionId });
expect(idSearchResponse).toBeTruthy();
expect(idSearchResponse.length).toEqual(1);

const idResult = idSearchResponse[0];
expect(idResult.transactionId).toEqual(transactionId);
if (isFailedTransaction(idResult)) {
throw new Error("Expected transaction to succeed");
}
expect(idResult.log).toMatch(/success/i);
const { transaction: idTransaction } = idResult;
if (!isSendTransaction(idTransaction)) {
throw new Error("Expected send transaction");
}
expect(idTransaction.kind).toEqual(unsigned.kind);
expect(idTransaction.sender).toEqual(unsigned.sender);
expect(idTransaction.recipient).toEqual(unsigned.recipient);
expect(idTransaction.memo).toEqual(unsigned.memo);
expect(idTransaction.amount).toEqual(unsigned.amount);

// search by sender address

const senderAddressSearchResponse = await connection.searchTx({ sentFromOrTo: faucetAddress });
expect(senderAddressSearchResponse).toBeTruthy();
expect(senderAddressSearchResponse.length).toBeGreaterThanOrEqual(1);

const senderAddressResult = senderAddressSearchResponse[senderAddressSearchResponse.length - 1];
expect(senderAddressResult.transactionId).toEqual(transactionId);
if (isFailedTransaction(senderAddressResult)) {
throw new Error("Expected transaction to succeed");
}
expect(senderAddressResult.log).toMatch(/success/i);
const { transaction: senderAddressTransaction } = senderAddressResult;
if (!isSendTransaction(senderAddressTransaction)) {
throw new Error("Expected send transaction");
}
expect(senderAddressTransaction.kind).toEqual(unsigned.kind);
expect(senderAddressTransaction.sender).toEqual(unsigned.sender);
expect(senderAddressTransaction.recipient).toEqual(unsigned.recipient);
expect(senderAddressTransaction.memo).toEqual(unsigned.memo);
expect(senderAddressTransaction.amount).toEqual(unsigned.amount);

// search by recipient address
// TODO: Support searching by recipient

// const recipientAddressSearchResponse = await connection.searchTx({ sentFromOrTo: defaultRecipient });
// expect(recipientAddressSearchResponse).toBeTruthy();
// expect(recipientAddressSearchResponse.length).toBeGreaterThanOrEqual(1);

// const recipientAddressResult =
// recipientAddressSearchResponse[recipientAddressSearchResponse.length - 1];
// expect(recipientAddressResult.transactionId).toEqual(transactionId);
// if (isFailedTransaction(recipientAddressResult)) {
// throw new Error("Expected transaction to succeed");
// }
// expect(recipientAddressResult.log).toMatch(/success/i);
// const { transaction: recipientAddressTransaction } = recipientAddressResult;
// if (!isSendTransaction(recipientAddressTransaction)) {
// throw new Error("Expected send transaction");
// }
// expect(recipientAddressTransaction.kind).toEqual(unsigned.kind);
// expect(recipientAddressTransaction.sender).toEqual(unsigned.sender);
// expect(recipientAddressTransaction.recipient).toEqual(unsigned.recipient);
// expect(recipientAddressTransaction.memo).toEqual(unsigned.memo);
// expect(recipientAddressTransaction.amount).toEqual(unsigned.amount);

// search by height

const heightSearchResponse = await connection.searchTx({ height: idResult.height });
expect(heightSearchResponse).toBeTruthy();
expect(heightSearchResponse.length).toEqual(1);

const heightResult = heightSearchResponse[0];
expect(heightResult.transactionId).toEqual(transactionId);
if (isFailedTransaction(heightResult)) {
throw new Error("Expected transaction to succeed");
}
expect(heightResult.log).toMatch(/success/i);
const { transaction: heightTransaction } = heightResult;
if (!isSendTransaction(heightTransaction)) {
throw new Error("Expected send transaction");
}
expect(heightTransaction.kind).toEqual(unsigned.kind);
expect(heightTransaction.sender).toEqual(unsigned.sender);
expect(heightTransaction.recipient).toEqual(unsigned.recipient);
expect(heightTransaction.memo).toEqual(unsigned.memo);
expect(heightTransaction.amount).toEqual(unsigned.amount);

connection.disconnect();
});
});
});
Loading