Pass prefix and token info into codec and connection
This commit is contained in:
parent
17468f5a75
commit
c40f07144a
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user