Pass prefix and token info into codec and connection

This commit is contained in:
Ethan Frey 2020-01-22 23:03:54 +01:00
parent 17468f5a75
commit c40f07144a
7 changed files with 94 additions and 65 deletions

View File

@ -92,7 +92,7 @@ export class CosmosCodec implements TxCodec {
}
const parsed = unmarshalTx(bytes);
// TODO: this needs access to token list
return parseTx(parsed, chainId, nonce);
return parseTx(parsed, chainId, nonce, this.tokens);
}
public identityToAddress(identity: Identity): Address {

View File

@ -16,6 +16,8 @@ import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol";
import { cosmosCodec } from "./cosmoscodec";
import { CosmosConnection } from "./cosmosconnection";
import { CosmosBech32Prefix } from "./address";
import { TokenInfos } from "./types";
const { fromBase64, toHex } = Encoding;
@ -40,10 +42,21 @@ describe("CosmosConnection", () => {
const faucetPath = HdPaths.cosmos(0);
const defaultRecipient = "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" as Address;
const defaultPrefix = "cosmos" as CosmosBech32Prefix;
const defaultTokens: TokenInfos = [
{
fractionalDigits: 6,
tokenName: "Atom (Cosmos Hub)",
tokenTicker: "ATOM" as TokenTicker,
denom: "uatom",
},
];
describe("establish", () => {
it("can connect to Cosmos via http", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
expect(connection).toBeTruthy();
connection.disconnect();
});
@ -52,7 +65,7 @@ describe("CosmosConnection", () => {
describe("chainId", () => {
it("displays the chain ID", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
const chainId = connection.chainId();
expect(chainId).toEqual(defaultChainId);
connection.disconnect();
@ -62,7 +75,7 @@ describe("CosmosConnection", () => {
describe("height", () => {
it("displays the current height", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
const height = await connection.height();
expect(height).toBeGreaterThan(0);
connection.disconnect();
@ -72,7 +85,7 @@ describe("CosmosConnection", () => {
describe("getToken", () => {
it("displays a given token", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
const token = await connection.getToken("cosm" as TokenTicker);
expect(token).toEqual({
fractionalDigits: 6,
@ -84,7 +97,7 @@ describe("CosmosConnection", () => {
it("resolves to undefined if the token is not supported", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
const token = await connection.getToken("whatever" as TokenTicker);
expect(token).toBeUndefined();
connection.disconnect();
@ -94,7 +107,7 @@ describe("CosmosConnection", () => {
describe("getAllTokens", () => {
it("resolves to a list of all supported tokens", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
const tokens = await connection.getAllTokens();
// TODO: make this more flexible
expect(tokens).toEqual([
@ -116,7 +129,7 @@ describe("CosmosConnection", () => {
describe("getAccount", () => {
it("gets an empty account by address", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
const account = await connection.getAccount({ address: defaultEmptyAddress });
expect(account).toBeUndefined();
connection.disconnect();
@ -124,7 +137,7 @@ describe("CosmosConnection", () => {
it("gets an account by address", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
const account = await connection.getAccount({ address: defaultAddress });
if (account === undefined) {
throw new Error("Expected account not to be undefined");
@ -138,7 +151,7 @@ describe("CosmosConnection", () => {
it("gets an account by pubkey", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
const account = await connection.getAccount({ pubkey: defaultPubkey });
if (account === undefined) {
throw new Error("Expected account not to be undefined");
@ -157,7 +170,7 @@ describe("CosmosConnection", () => {
describe("integration tests", () => {
it("can post and get a transaction", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic));
const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath);
@ -216,7 +229,7 @@ describe("CosmosConnection", () => {
it("can post and search for a transaction", async () => {
pendingWithoutCosmos();
const connection = await CosmosConnection.establish(httpUrl);
const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens);
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic));
const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath);

View File

@ -38,6 +38,7 @@ import { CosmosBech32Prefix, pubkeyToAddress } from "./address";
import { Caip5 } from "./caip5";
import { decodeAmount, parseTxsResponse } from "./decode";
import { RestClient, TxsResponse } from "./restclient";
import { TokenInfos } from "./types";
const { fromBase64 } = Encoding;
@ -70,10 +71,15 @@ function buildQueryString({
}
export class CosmosConnection implements BlockchainConnection {
public static async establish(url: string): Promise<CosmosConnection> {
// we must know prefix and tokens a priori to understand the chain
public static async establish(
url: string,
prefix: CosmosBech32Prefix,
tokenInfo: TokenInfos,
): Promise<CosmosConnection> {
const restClient = new RestClient(url);
const chainData = await this.initialize(restClient);
return new CosmosConnection(restClient, chainData);
return new CosmosConnection(restClient, chainData, prefix, tokenInfo);
}
private static async initialize(restClient: RestClient): Promise<ChainData> {
@ -84,29 +90,34 @@ export class CosmosConnection implements BlockchainConnection {
private readonly restClient: RestClient;
private readonly chainData: ChainData;
private readonly primaryToken: Token;
// TODO: deprecate this???
private readonly supportedTokens: readonly Token[];
private readonly _prefix: CosmosBech32Prefix;
private readonly tokenInfo: TokenInfos;
private get prefix(): CosmosBech32Prefix {
return "cosmos";
return this._prefix;
}
private constructor(restClient: RestClient, chainData: ChainData) {
private constructor(
restClient: RestClient,
chainData: ChainData,
prefix: CosmosBech32Prefix,
tokenInfo: TokenInfos,
) {
this.restClient = restClient;
this.chainData = chainData;
// TODO: this is an argument
this.primaryToken = {
fractionalDigits: 6,
tokenName: "Cosm",
tokenTicker: "cosm" as TokenTicker,
};
this.supportedTokens = [
this.primaryToken,
{
fractionalDigits: 6,
tokenName: "Stake",
tokenTicker: "stake" as TokenTicker,
},
];
this._prefix = prefix;
this.tokenInfo = tokenInfo;
this.supportedTokens = this.tokenInfo.map(info => ({
tokenTicker: info.tokenTicker,
tokenName: info.tokenName,
fractionalDigits: info.fractionalDigits,
}));
this.primaryToken = this.supportedTokens[0];
}
public disconnect(): void {
@ -135,16 +146,13 @@ export class CosmosConnection implements BlockchainConnection {
const { result } = await this.restClient.authAccounts(address);
const account = result.value;
const supportedCoins = account.coins.filter(({ denom }) =>
this.supportedTokens.find(
// TODO: ugly special case - fix this
({ tokenTicker }) => (tokenTicker === "ATOM" && denom === "uatom") || tokenTicker === denom,
),
this.tokenInfo.find(token => token.denom === denom),
);
return account.public_key === null
? undefined
: {
address: address,
balance: supportedCoins.map(decodeAmount),
balance: supportedCoins.map(decodeAmount(this.tokenInfo)),
pubkey: {
algo: Algorithm.Secp256k1,
data: fromBase64(account.public_key.value) as PubkeyBytes,
@ -289,6 +297,6 @@ export class CosmosConnection implements BlockchainConnection {
const sender = (response.tx.value as any).msg[0].value.from_address;
const accountForHeight = await this.restClient.authAccounts(sender, response.height);
const nonce = (parseInt(accountForHeight.result.value.sequence, 10) - 1) as Nonce;
return parseTxsResponse(chainId, parseInt(response.height, 10), nonce, response);
return parseTxsResponse(chainId, parseInt(response.height, 10), nonce, response, this.tokenInfo);
}
}

View File

@ -2,16 +2,20 @@ import { ChainConnector, ChainId } from "@iov/bcp";
import { cosmosCodec } from "./cosmoscodec";
import { CosmosConnection } from "./cosmosconnection";
import { CosmosBech32Prefix } from "./address";
import { TokenInfos } from "./types";
/**
* A helper to connect to a cosmos-based chain at a given url
*/
export function createCosmosConnector(
url: string,
prefix: CosmosBech32Prefix,
tokenInfo: TokenInfos,
expectedChainId?: ChainId,
): ChainConnector<CosmosConnection> {
return {
establishConnection: async () => CosmosConnection.establish(url),
establishConnection: async () => CosmosConnection.establish(url, prefix, tokenInfo),
codec: cosmosCodec,
expectedChainId: expectedChainId,
};

View File

@ -15,9 +15,19 @@ import {
} from "./decode";
import { chainId, nonce, signedTxJson, txId } from "./testdata.spec";
import data from "./testdata/cosmoshub.json";
import { TokenInfos } from "./types";
const { fromBase64 } = Encoding;
const defaultTokens: TokenInfos = [
{
fractionalDigits: 6,
tokenName: "Atom (Cosmos Hub)",
tokenTicker: "ATOM" as TokenTicker,
denom: "uatom",
},
];
describe("decode", () => {
const defaultPubkey = {
algo: Algorithm.Secp256k1,
@ -89,7 +99,7 @@ describe("decode", () => {
denom: "uatom",
amount: "11657995",
};
expect(decodeAmount(amount)).toEqual(defaultAmount);
expect(decodeAmount(defaultTokens)(amount)).toEqual(defaultAmount);
});
});
@ -108,7 +118,7 @@ describe("decode", () => {
],
},
};
expect(parseMsg(msg, chainId)).toEqual(defaultSendTransaction);
expect(parseMsg(msg, chainId, defaultTokens)).toEqual(defaultSendTransaction);
});
});
@ -123,13 +133,13 @@ describe("decode", () => {
],
gas: "200000",
};
expect(parseFee(fee)).toEqual(defaultFee);
expect(parseFee(fee, defaultTokens)).toEqual(defaultFee);
});
});
describe("parseTx", () => {
it("works", () => {
expect(parseTx(data.tx, chainId, nonce)).toEqual(signedTxJson);
expect(parseTx(data.tx, chainId, nonce, defaultTokens)).toEqual(signedTxJson);
});
});
@ -149,7 +159,7 @@ describe("decode", () => {
transactionId: txId,
log: '[{"msg_index":0,"success":true,"log":""}]',
};
expect(parseTxsResponse(chainId, currentHeight, nonce, txsResponse)).toEqual(expected);
expect(parseTxsResponse(chainId, currentHeight, nonce, txsResponse, defaultTokens)).toEqual(expected);
});
});
});

View File

@ -20,7 +20,7 @@ import { Encoding } from "@iov/encoding";
import amino from "@tendermint/amino-js";
import { TxsResponse } from "./restclient";
import { isAminoStdTx } from "./types";
import { isAminoStdTx, TokenInfos, coinToAmount } from "./types";
const { fromBase64 } = Encoding;
@ -47,20 +47,13 @@ export function decodeFullSignature(signature: amino.StdSignature, nonce: number
// TODO: this needs access to token list - we need something more like amountToCoin and coinToAmount here
// and wire that info all the way from both connection and codec.
export function decodeAmount(amount: amino.Coin): Amount {
// TODO: more uglyness here (breaks unit tests)
if (amount.denom !== "uatom") {
throw new Error("Only ATOM amounts are supported");
}
return {
fractionalDigits: 6,
quantity: amount.amount,
tokenTicker: atom,
// tokenTicker: amount.denom as TokenTicker,
};
}
export function parseMsg(msg: amino.Msg, chainId: ChainId): SendTransaction {
// TODO: return null vs throw exception for undefined???
export const decodeAmount = (tokens: TokenInfos) => (coin: amino.Coin): Amount => {
return coinToAmount(tokens, coin);
};
export function parseMsg(msg: amino.Msg, chainId: ChainId, tokens: TokenInfos): SendTransaction {
if (msg.type !== "cosmos-sdk/MsgSend") {
throw new Error("Unknown message type in transaction");
}
@ -77,21 +70,21 @@ export function parseMsg(msg: amino.Msg, chainId: ChainId): SendTransaction {
sender: msgValue.from_address as Address,
recipient: msgValue.to_address as Address,
// TODO: this needs access to token list
amount: decodeAmount(msgValue.amount[0]),
amount: decodeAmount(tokens)(msgValue.amount[0]),
};
}
export function parseFee(fee: amino.StdFee): Fee {
export function parseFee(fee: amino.StdFee, tokens: TokenInfos): Fee {
if (fee.amount.length !== 1) {
throw new Error("Only fee with one amount is supported");
}
return {
tokens: decodeAmount(fee.amount[0]),
tokens: decodeAmount(tokens)(fee.amount[0]),
gasLimit: fee.gas,
};
}
export function parseTx(tx: amino.Tx, chainId: ChainId, nonce: Nonce): SignedTransaction {
export function parseTx(tx: amino.Tx, chainId: ChainId, nonce: Nonce, tokens: TokenInfos): SignedTransaction {
const txValue = tx.value;
if (!isAminoStdTx(txValue)) {
throw new Error("Only Amino StdTx is supported");
@ -102,9 +95,9 @@ export function parseTx(tx: amino.Tx, chainId: ChainId, nonce: Nonce): SignedTra
const [primarySignature] = txValue.signatures.map(signature => decodeFullSignature(signature, nonce));
// TODO: this needs access to token list
const msg = parseMsg(txValue.msg[0], chainId);
const msg = parseMsg(txValue.msg[0], chainId, tokens);
// TODO: this needs access to token list
const fee = parseFee(txValue.fee);
const fee = parseFee(txValue.fee, tokens);
const transaction = {
...msg,
@ -124,10 +117,11 @@ export function parseTxsResponse(
currentHeight: number,
nonce: Nonce,
response: TxsResponse,
tokens: TokenInfos,
): ConfirmedAndSignedTransaction<UnsignedTransaction> {
const height = parseInt(response.height, 10);
return {
...parseTx(response.tx, chainId, nonce),
...parseTx(response.tx, chainId, nonce, tokens),
height: height,
confirmations: currentHeight - height + 1,
transactionId: response.txhash as TransactionId,

View File

@ -28,8 +28,8 @@ export function amountToCoin(lookup: ReadonlyArray<TokenInfo>, amount: Amount):
};
}
export function coinToAmount(lookup: ReadonlyArray<TokenInfo>, coin: amino.Coin): Amount {
const match = lookup.find(({ denom }) => denom === coin.denom);
export function coinToAmount(tokens: TokenInfos, coin: amino.Coin): Amount {
const match = tokens.find(({ denom }) => denom === coin.denom);
if (!match) {
throw Error(`unknown denom: ${coin.denom}`);
}