cosmjs-util/packages/bcp/src/decode.ts

212 lines
5.7 KiB
TypeScript

import { IndexedTx, types } from "@cosmwasm/sdk";
import {
Address,
Algorithm,
Amount,
ChainId,
ConfirmedAndSignedTransaction,
ConfirmedTransaction,
Fee,
FullSignature,
Nonce,
PubkeyBundle,
PubkeyBytes,
SendTransaction,
SignatureBytes,
SignedTransaction,
TokenTicker,
TransactionId,
UnsignedTransaction,
} from "@iov/bcp";
import { Decimal, Encoding } from "@iov/encoding";
import BN from "bn.js";
import { BankTokens, Erc20Token } from "./types";
const { fromBase64 } = Encoding;
export function decodePubkey(pubkey: types.PubKey): PubkeyBundle {
switch (pubkey.type) {
case types.pubkeyType.secp256k1:
return {
algo: Algorithm.Secp256k1,
data: fromBase64(pubkey.value) as PubkeyBytes,
};
case types.pubkeyType.ed25519:
return {
algo: Algorithm.Ed25519,
data: fromBase64(pubkey.value) as PubkeyBytes,
};
default:
throw new Error("Unsupported pubkey type");
}
}
export function decodeSignature(signature: string): SignatureBytes {
return fromBase64(signature) as SignatureBytes;
}
export function decodeFullSignature(signature: types.StdSignature, nonce: number): FullSignature {
return {
nonce: nonce as Nonce,
pubkey: decodePubkey(signature.pub_key),
signature: decodeSignature(signature.signature),
};
}
export function coinToDecimal(tokens: BankTokens, coin: types.Coin): readonly [Decimal, string] {
const match = tokens.find(({ denom }) => denom === coin.denom);
if (!match) {
throw Error(`unknown denom: ${coin.denom}`);
}
const value = Decimal.fromAtomics(coin.amount, match.fractionalDigits);
return [value, match.ticker];
}
export function decodeAmount(tokens: BankTokens, coin: types.Coin): Amount {
const [value, ticker] = coinToDecimal(tokens, coin);
return {
quantity: value.atomics,
fractionalDigits: value.fractionalDigits,
tokenTicker: ticker as TokenTicker,
};
}
export function parseMsg(
msg: types.Msg,
memo: string | undefined,
chainId: ChainId,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): UnsignedTransaction {
if (types.isMsgSend(msg)) {
if (msg.value.amount.length !== 1) {
throw new Error("Only MsgSend with one amount is supported");
}
const send: SendTransaction = {
kind: "bcp/send",
chainId: chainId,
sender: msg.value.from_address as Address,
recipient: msg.value.to_address as Address,
amount: decodeAmount(tokens, msg.value.amount[0]),
memo: memo,
};
return send;
} else if (types.isMsgExecuteContract(msg)) {
const matchingTokenContract = erc20Tokens.find(t => t.contractAddress === msg.value.contract);
if (!matchingTokenContract) {
return {
chainId: chainId,
kind: "bcp/unknown",
};
}
const recipient: string | undefined = msg.value.msg.transfer?.recipient;
if (!recipient) throw new Error("Could not read recipient");
const amount: string | undefined = msg.value.msg.transfer?.amount;
if (!amount) throw new Error("Could not read recipient");
const send: SendTransaction = {
kind: "bcp/send",
chainId: chainId,
sender: msg.value.sender as Address,
recipient: recipient as Address,
amount: {
quantity: new BN(amount).toString(),
fractionalDigits: matchingTokenContract.fractionalDigits,
tokenTicker: matchingTokenContract.ticker as TokenTicker,
},
memo: memo,
};
return send;
} else {
// Unknown transaction type
const unknown = {
chainId: chainId,
kind: "bcp/unknown",
};
return unknown;
}
}
export function parseFee(fee: types.StdFee, tokens: BankTokens): Fee {
if (fee.amount.length !== 1) {
throw new Error("Only fee with one amount is supported");
}
return {
tokens: decodeAmount(tokens, fee.amount[0]),
gasLimit: fee.gas,
};
}
export function parseUnsignedTx(
txValue: types.StdTx,
chainId: ChainId,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): UnsignedTransaction {
if (!types.isStdTx(txValue)) {
throw new Error("Only StdTx is supported");
}
if (txValue.msg.length !== 1) {
throw new Error("Only single-message transactions currently supported");
}
const msg = parseMsg(txValue.msg[0], txValue.memo, chainId, tokens, erc20Tokens);
const fee = parseFee(txValue.fee, tokens);
return {
...msg,
chainId: chainId,
fee: fee,
};
}
export function parseSignedTx(
txValue: types.StdTx,
chainId: ChainId,
nonce: Nonce,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): SignedTransaction {
const [primarySignature] = txValue.signatures.map(signature => decodeFullSignature(signature, nonce));
return {
transaction: parseUnsignedTx(txValue, chainId, tokens, erc20Tokens),
signatures: [primarySignature],
};
}
export function parseTxsResponseUnsigned(
chainId: ChainId,
currentHeight: number,
response: IndexedTx,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): ConfirmedTransaction<UnsignedTransaction> {
return {
transaction: parseUnsignedTx(response.tx.value, chainId, tokens, erc20Tokens),
height: response.height,
confirmations: currentHeight - response.height + 1,
transactionId: response.hash as TransactionId,
log: response.rawLog,
};
}
export function parseTxsResponseSigned(
chainId: ChainId,
currentHeight: number,
nonce: Nonce,
response: IndexedTx,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): ConfirmedAndSignedTransaction<UnsignedTransaction> {
return {
...parseSignedTx(response.tx.value, chainId, nonce, tokens, erc20Tokens),
height: response.height,
confirmations: currentHeight - response.height + 1,
transactionId: response.hash as TransactionId,
log: response.rawLog,
};
}