@dojima-wallet/connection
Version:
Initialise and connection for layer 1&2 blockchain
1,224 lines (1,093 loc) • 34.7 kB
text/typescript
import { cosmosclient, proto } from "@cosmos-client/core";
import {
Balance,
BaseChainClient,
ChainClient,
ChainClientParams,
FeeType,
Fees,
Network,
Tx,
TxFrom,
TxHash,
TxHistoryParams,
TxParams,
TxTo,
TxType,
TxsPage,
singleFee,
} from "../client";
import { CosmosSDKClient, RPCTxResult } from "../cosmos";
import {
Address,
Asset,
AssetDOJNative,
BaseAmount,
CosmosChain,
assetFromString,
assetToString,
baseAmount,
isAssetDOJNative,
} from "@dojima-wallet/utils";
import axios from "axios";
import BigNumber from "bignumber.js";
import Long from "long";
import { buildDepositTx, buildTransferTx, buildUnsignedTx } from ".";
import { TxResult } from "./messages";
import {
ChainId,
ChainIds,
ClientUrl,
DepositParam,
ExplorerUrls,
HermeschainClientParams,
HermeschainConstantsResponse,
NodeUrl,
TxData,
TxOfflineParams,
VersionParam,
IpAddressParam,
NodePubkeyParam,
CreateOperatorParam,
RegisterChainParam,
CreateEndpointParam,
} from "./types";
import {
DEFAULT_GAS_LIMIT_VALUE,
DEPOSIT_GAS_LIMIT_VALUE,
DOJ_DECIMAL,
MAX_TX_COUNT,
getBalance,
getDefaultFees,
getDenom,
getDepositTxDataFromLogs,
getExplorerAddressUrl,
getExplorerTxUrl,
getPrefix,
registerCreateOperatorCodecs,
registerRegisterChainCodecs,
registerCreateEndpointCodecs,
registerDepositCodecs,
registerSendCodecs,
buildSetVersionTx,
registerSetVersionCodecs,
buildSetIpAddressTx,
registerSetIpAddrCodecs,
buildSetPubkeysTx,
registerSetNodePubkeysCodecs,
buildCreateOperatorTx,
buildRegisterChainTx,
buildCreateEndpointTx,
} from "./util";
import {
calcDoubleSwapOutput,
calcDoubleSwapSlip,
calcSwapOutput,
calcSwapSlip,
PoolData,
SwapFeeResult,
} from "../swap_utils";
/**
* Interface for custom Hermeschain client
*/
export interface HermeschainClient {
setClientUrl(clientUrl: ClientUrl): void;
getClientUrl(): NodeUrl;
setExplorerUrls(explorerUrls: ExplorerUrls): void;
getCosmosClient(): CosmosSDKClient;
deposit(params: DepositParam): Promise<TxHash>;
createOperator(params: CreateOperatorParam): Promise<TxHash>;
createEndpoint(params: CreateEndpointParam): Promise<TxHash>;
registerChain(params: RegisterChainParam): Promise<TxHash>;
transferOffline(params: TxOfflineParams): Promise<string>;
}
/**
* Custom Hermeschain Client
*/
class HermesClient
extends BaseChainClient
implements HermeschainClient, ChainClient
{
private clientUrl: ClientUrl;
private explorerUrls: ExplorerUrls;
private chainIds: ChainIds;
private cosmosClient: CosmosSDKClient;
private apiUrl: string;
private rpcUrl: string;
/**
* Constructor
*
* Client has to be initialised with network type and phrase.
* It will throw an error if an invalid phrase has been passed.
*
* @param {ChainClientParams} params
*
* @throws {"Invalid phrase"} Thrown if the given phase is invalid.
*/
constructor({
network = Network.Mainnet,
phrase,
apiUrl,
rpcUrl,
rootDerivationPaths = {
[Network.Mainnet]: "44'/187'/0'/0/",
[Network.Stagenet]: "44'/187'/0'/0/",
[Network.Testnet]: "44'/184'/0'/0/",
},
}: ChainClientParams & HermeschainClientParams) {
super(CosmosChain, { network, rootDerivationPaths, phrase });
this.apiUrl = apiUrl;
this.rpcUrl = rpcUrl;
this.clientUrl = this.getDefaultClientUrls();
this.explorerUrls = this.getDefaultExplorerUrls();
this.chainIds = this.getDefaultChainIds();
registerSendCodecs();
registerCreateOperatorCodecs();
registerRegisterChainCodecs();
registerCreateEndpointCodecs();
registerDepositCodecs();
registerSetVersionCodecs();
registerSetNodePubkeysCodecs();
registerSetIpAddrCodecs();
this.cosmosClient = new CosmosSDKClient({
server: this.getClientUrl().node,
chainId: this.getChainId(network),
prefix: getPrefix(network),
});
}
/**
* Get default chainId's
* */
getDefaultChainIds(): ChainIds {
return {
[Network.Mainnet]: "h4s-187-d11k",
[Network.Stagenet]: "h4s-187-d11k",
// [Network.Testnet]: 'h4s-184-d11k',
[Network.Testnet]:
this.apiUrl.includes("api-test-h4s") || this.apiUrl.includes("api-h4s")
? "h4s-184-d11k"
: "hermeschain",
};
}
/**
* Get default client url's
* */
getDefaultClientUrls(): ClientUrl {
return {
[Network.Testnet]: {
node: this.apiUrl,
rpc: this.rpcUrl,
},
[Network.Stagenet]: {
node: this.apiUrl,
rpc: this.rpcUrl,
},
[Network.Mainnet]: {
node: this.apiUrl,
rpc: this.rpcUrl,
},
};
}
/**
* Get default Explorer Url's
* */
getDefaultExplorerUrls(): ExplorerUrls {
const txUrl = `${this.apiUrl}/tx`;
const addressUrl = `${this.apiUrl}/address`;
return {
root: {
[Network.Testnet]: `${this.apiUrl}?network=testnet`,
[Network.Stagenet]: `${this.apiUrl}?network=stagenet`,
[Network.Mainnet]: this.apiUrl,
},
tx: {
[Network.Testnet]: txUrl,
[Network.Stagenet]: txUrl,
[Network.Mainnet]: txUrl,
},
address: {
[Network.Testnet]: addressUrl,
[Network.Stagenet]: addressUrl,
[Network.Mainnet]: addressUrl,
},
};
}
/**
* Set/update the current network.
*
* @param {Network} network
* @returns {void}
*
* @throws {"Network must be provided"}
* Thrown if network has not been set before.
*/
setNetwork(network: Network): void {
// dirty check to avoid using and re-creation of same data
if (network === this.network) return;
super.setNetwork(network);
this.cosmosClient = new CosmosSDKClient({
server: this.getClientUrl().node,
chainId: this.getChainId(network),
prefix: getPrefix(network),
});
}
/**
* Set/update the client URL.
*
* @param {ClientUrl} clientUrl The client url to be set.
* @returns {void}
*/
setClientUrl(clientUrl: ClientUrl): void {
this.clientUrl = clientUrl;
}
/**
* Get the client url.
*
* @returns {NodeUrl} The client url for hermeschain based on the current network.
*/
getClientUrl(): NodeUrl {
return this.clientUrl[this.network];
}
/**
* Set/update the explorer URLs.
*
* @param {ExplorerUrls} urls The explorer urls to be set.
* @returns {void}
*/
setExplorerUrls(urls: ExplorerUrls): void {
this.explorerUrls = urls;
}
/**
* Get the explorer url.
*
* @returns {string} The explorer url for hermeschain based on the current network.
*/
getExplorerUrl(): string {
return this.explorerUrls.root[this.network];
}
/**
* Sets chain id
*
* @param {ChainId} chainId Chain id to update
* @param {Network} network (optional) Network for given chainId. If `network`not set, current network of the client is used
*
* @returns {void}
*/
setChainId(chainId: ChainId, network?: Network): void {
this.chainIds = { ...this.chainIds, [network || this.network]: chainId };
}
/**
* Gets chain id
*
* @param {Network} network (optional) Network to get chain id from. If `network`not set, current network of the client is used
*
* @returns {ChainId} Chain id based on the current network.
*/
getChainId(network?: Network): ChainId {
return this.chainIds[network || this.network];
}
/**
* Get cosmos client
* @returns {CosmosSDKClient} current cosmos client
*/
getCosmosClient(): CosmosSDKClient {
return this.cosmosClient;
}
/**
* Get the explorer url for the given address.
*
* @param {Address} address
* @returns {string} The explorer url for the given address.
*/
getExplorerAddressUrl(address: Address): string {
return getExplorerAddressUrl({
urls: this.explorerUrls,
network: this.network,
address,
});
}
/**
* Get the explorer url for the given transaction id.
*
* @param {string} txID
* @returns {string} The explorer url for the given transaction id.
*/
getExplorerTxUrl(txID: string): string {
return getExplorerTxUrl({
urls: this.explorerUrls,
network: this.network,
txID,
});
}
/**
* Get private key
*
* @param {number} index the HD wallet index (optional)
* @returns {PrivKey} The private key generated from the given phrase
*
* @throws {"Phrase not set"}
* Throws an error if phrase has not been set before
* */
getPrivateKey(index = 0): proto.cosmos.crypto.secp256k1.PrivKey {
return this.cosmosClient.getPrivKeyFromMnemonic(
this.phrase,
this.getFullDerivationPath(index)
);
}
/**
* Get public key
*
* @param {number} index the HD wallet index (optional)
*
* @returns {PubKey} The public key generated from the given phrase
*
* @throws {"Phrase not set"}
* Throws an error if phrase has not been set before
**/
getPubKey(index = 0): cosmosclient.PubKey {
const privKey = this.getPrivateKey(index);
return privKey.pubKey();
}
/**
* Get secondary root derivation path required for account retrieval instead of default
*/
private getSecondaryAccountRootDerivationPath({
walletIndex = 0,
secondaryAccountIndex = 0,
}: {
walletIndex: number;
secondaryAccountIndex: number;
}): string {
const secondaryRootDerivationPaths = {
[Network.Mainnet]: `44'/187'/${secondaryAccountIndex}'/0/`,
[Network.Stagenet]: `44'/187'/${secondaryAccountIndex}'/0/`,
[Network.Testnet]: `44'/184'/${secondaryAccountIndex}'/0/`,
};
return `${secondaryRootDerivationPaths[this.network]}${walletIndex}`;
}
/**
* Get the current address.
*
* @returns {Address} The current address.
*
* @throws {Error} Thrown if phrase has not been set before. A phrase is needed to create a wallet and to derive an address from it.
*/
getAddress(index = 0, secondaryAccountIndex = 0): string {
const address = this.cosmosClient.getAddressFromMnemonic(
this.phrase,
secondaryAccountIndex !== 0
? this.getSecondaryAccountRootDerivationPath({
walletIndex: index,
secondaryAccountIndex,
})
: this.getFullDerivationPath(index)
);
if (!address) {
throw new Error("address not defined");
}
return address;
}
/**
* Validate the given address.
*
* @param {Address} address
* @returns {boolean} `true` or `false`
*/
validateAddress(address: Address): boolean {
return this.cosmosClient.checkAddress(address);
}
/**
* Get the balance of a given address.
*
* @param {Address} address By default, it will return the balance of the current wallet. (optional)
* @param {Asset} asset If not set, it will return all assets available. (optional)
* @returns {Balance[]} The balance of the address.
*/
async getBalance(address: Address, assets?: Asset[]): Promise<Balance[]> {
return getBalance({
address,
assets,
cosmosClient: this.getCosmosClient(),
});
}
/**
* Get transaction history of a given address with pagination options.
* By default, it will return the transaction history of the current wallet.
*
* @param {TxHistoryParams} params The options to get transaction history. (optional)
* @returns {TxsPage} The transaction history.
*/
getTransactions = async (
params?: TxHistoryParams & { filterFn?: (tx: RPCTxResult) => boolean }
): Promise<TxsPage> => {
const messageAction: any = undefined;
const offset = params?.offset || 0;
const limit = params?.limit || 10;
const address = params?.address || this.getAddress();
const txMinHeight: any = undefined;
const txMaxHeight: any = undefined;
const txIncomingHistory = (
await this.cosmosClient.searchTxFromRPC({
rpcEndpoint: this.getClientUrl().rpc,
messageAction,
transferRecipient: address,
limit: MAX_TX_COUNT,
txMinHeight,
txMaxHeight,
})
).txs;
const txOutgoingHistory = (
await this.cosmosClient.searchTxFromRPC({
rpcEndpoint: this.getClientUrl().rpc,
messageAction,
transferSender: address,
limit: MAX_TX_COUNT,
txMinHeight,
txMaxHeight,
})
).txs;
let history: RPCTxResult[] = txIncomingHistory
.concat(txOutgoingHistory)
.sort((a, b) => {
if (a.height !== b.height)
return parseInt(b.height) > parseInt(a.height) ? 1 : -1;
if (a.hash !== b.hash) return a.hash > b.hash ? 1 : -1;
return 0;
})
.reduce(
(acc, tx) => [
...acc,
...(acc.length === 0 || acc[acc.length - 1].hash !== tx.hash
? [tx]
: []),
],
[] as RPCTxResult[]
)
.filter(params?.filterFn ? params.filterFn : (tx) => tx)
.filter((_, index) => index < MAX_TX_COUNT);
// get `total` before filtering txs out for pagination
const total = history.length;
history = history.filter(
(_, index) => index >= offset && index < offset + limit
);
const txs = await Promise.all(
history.map(({ hash }) => this.getTransactionData(hash, address))
);
return {
total,
txs,
};
};
/**
* Get the transaction details of a given transaction id.
*
* @param {string} txId The transaction id.
* @returns {Tx} The transaction details of the given transaction id.
*/
async getTransactionData(txId: string, address: Address): Promise<Tx> {
const txResult = await this.cosmosClient.txsHashGet(txId);
const txData: TxData | null =
txResult && txResult.logs
? getDepositTxDataFromLogs(txResult.logs, address)
: null;
if (!txResult || !txData)
throw new Error(`Failed to get transaction data (tx-hash: ${txId})`);
const { from, to, type } = txData;
return {
hash: txId,
asset: AssetDOJNative,
from,
to,
date: new Date(txResult.timestamp),
type,
};
}
/**
* Get the transaction details of a given transaction id. (from /hermeschain/txs/hash)
*
* Node: /hermeschain/txs/hash response doesn't have timestamp field.
*
* @param {string} txId The transaction id.
* @returns {Tx} The transaction details of the given transaction id.
*/
async getDepositTransaction(txId: string): Promise<Omit<Tx, "date">> {
const result: TxResult = (
await axios.get(`${this.getClientUrl().node}/hermeschain/tx/${txId}`)
).data;
if (!result || !result.observed_tx)
throw new Error("transaction not found");
const from: TxFrom[] = [];
const to: TxTo[] = [];
let asset;
result.observed_tx.tx.coins.forEach((coin) => {
from.push({
from: result.observed_tx.tx.from_address,
amount: baseAmount(coin.amount, DOJ_DECIMAL),
});
to.push({
to: result.observed_tx.tx.to_address,
amount: baseAmount(coin.amount, DOJ_DECIMAL),
});
asset = assetFromString(coin.asset);
});
return {
asset: asset || AssetDOJNative,
from,
to,
type: TxType.Transfer,
hash: txId,
};
}
async createOperator({
walletIndex = 0,
serverAddress,
stakeAmount,
gasLimit = new BigNumber(DEPOSIT_GAS_LIMIT_VALUE),
}: CreateOperatorParam): Promise<TxHash> {
const balances = await this.getBalance(this.getAddress(walletIndex));
const dojBalance: BaseAmount =
balances.filter(({ asset }) => isAssetDOJNative(asset))[0]?.amount ??
baseAmount(0, DOJ_DECIMAL);
const asset = AssetDOJNative;
const assetBalance: BaseAmount =
balances.filter(
({ asset: assetInList }) =>
assetToString(assetInList) === assetToString(asset)
)[0]?.amount ?? baseAmount(0, DOJ_DECIMAL);
const { average: fee } = await this.getFees();
if (isAssetDOJNative(asset)) {
// amount + fee < dojBalance
if (dojBalance.lt(stakeAmount.plus(fee))) {
throw new Error("insufficient funds");
}
} else {
// amount < assetBalances && dojBalance < fee
if (assetBalance.lt(stakeAmount) || dojBalance.lt(fee)) {
throw new Error("insufficient funds");
}
}
const privKey = this.getPrivateKey(walletIndex);
const signerPubkey = privKey.pubKey();
const fromAddress = this.getAddress(walletIndex);
const fromAddressAcc = cosmosclient.AccAddress.fromString(fromAddress);
const createOperatorTxBody = await buildCreateOperatorTx({
msgCreateOperator: {
signer: fromAddressAcc,
stakeAmount: stakeAmount.amount().toString(),
serverAddress: serverAddress,
},
nodeUrl: this.getClientUrl().node,
chainId: this.getChainId(),
});
const account = await this.getCosmosClient().getAccount(fromAddressAcc);
const { account_number: accountNumber } = account;
if (!accountNumber) {
throw Error(
`Create operator failed - could not get account number ${accountNumber}`
);
}
const txBuilder = buildUnsignedTx({
cosmosSdk: this.getCosmosClient().sdk,
txBody: createOperatorTxBody,
signerPubkey: cosmosclient.codec.instanceToProtoAny(signerPubkey),
gasLimit: Long.fromString(gasLimit.toFixed(0)),
sequence: account.sequence || Long.ZERO,
});
const txHash = await this.getCosmosClient().signAndBroadcast(
txBuilder,
privKey,
accountNumber
);
if (!txHash) throw Error(`Invalid transaction hash: ${txHash}`);
return txHash;
}
async registerChain({
walletIndex = 0,
chain,
cmpUnits,
gasLimit = new BigNumber(DEPOSIT_GAS_LIMIT_VALUE),
}: RegisterChainParam): Promise<TxHash> {
const privKey = this.getPrivateKey(walletIndex);
const signerPubkey = privKey.pubKey();
const fromAddress = this.getAddress(walletIndex);
const fromAddressAcc = cosmosclient.AccAddress.fromString(fromAddress);
// if chain. id is not defined, throw error
if (!chain.chainId) {
throw new Error("chain id is not defined");
}
const registerChainTxBody = await buildRegisterChainTx({
msgRegisterChain: {
chain: chain,
computeUnits: cmpUnits,
signer: fromAddressAcc,
},
nodeUrl: this.getClientUrl().node,
chainId: this.getChainId(),
});
const account = await this.getCosmosClient().getAccount(fromAddressAcc);
const { account_number: accountNumber } = account;
if (!accountNumber) {
throw Error(
`Register chain failed - could not get account number ${accountNumber}`
);
}
const txBuilder = buildUnsignedTx({
cosmosSdk: this.getCosmosClient().sdk,
txBody: registerChainTxBody,
signerPubkey: cosmosclient.codec.instanceToProtoAny(signerPubkey),
gasLimit: Long.fromString(gasLimit.toFixed(0)),
sequence: account.sequence || Long.ZERO,
});
const txHash = await this.getCosmosClient().signAndBroadcast(
txBuilder,
privKey,
accountNumber
);
if (!txHash) throw Error(`Invalid transaction hash: ${txHash}`);
return txHash;
}
async createEndpoint({
walletIndex = 0,
chain,
rpcUrl,
wsUrl,
gasLimit = new BigNumber(DEPOSIT_GAS_LIMIT_VALUE),
}: CreateEndpointParam): Promise<TxHash> {
const privKey = this.getPrivateKey(walletIndex);
const signerPubkey = privKey.pubKey();
const fromAddress = this.getAddress(walletIndex);
const fromAddressAcc = cosmosclient.AccAddress.fromString(fromAddress);
// if chain. id is not defined, throw error
if (!chain.chainId) {
throw new Error("chain id is not defined");
}
const createEndpointTxBody = await buildCreateEndpointTx({
msgCreateEndpoint: {
chain: chain,
rpcUrl: rpcUrl,
wsUrl: wsUrl,
signer: fromAddressAcc,
},
nodeUrl: this.getClientUrl().node,
chainId: this.getChainId(),
});
const account = await this.getCosmosClient().getAccount(fromAddressAcc);
const { account_number: accountNumber } = account;
if (!accountNumber) {
throw Error(
`Create endpoint failed - could not get account number ${accountNumber}`
);
}
const txBuilder = buildUnsignedTx({
cosmosSdk: this.getCosmosClient().sdk,
txBody: createEndpointTxBody,
signerPubkey: cosmosclient.codec.instanceToProtoAny(signerPubkey),
gasLimit: Long.fromString(gasLimit.toFixed(0)),
sequence: account.sequence || Long.ZERO,
});
const txHash = await this.getCosmosClient().signAndBroadcast(
txBuilder,
privKey,
accountNumber
);
if (!txHash) throw Error(`Invalid transaction hash: ${txHash}`);
return txHash;
}
/**
* Transaction with MsgNativeTx.
*
* @param {DepositParam} params The transaction options.
* @returns {TxHash} The transaction hash.
*
* @throws {"insufficient funds"} Thrown if the wallet has insufficient funds.
* @throws {"Invalid transaction hash"} Thrown by missing tx hash
*/
async deposit({
walletIndex = 0,
asset = AssetDOJNative,
amount,
memo,
gasLimit = new BigNumber(DEPOSIT_GAS_LIMIT_VALUE),
}: DepositParam): Promise<TxHash> {
const balances = await this.getBalance(this.getAddress(walletIndex));
const dojBalance: BaseAmount =
balances.filter(({ asset }) => isAssetDOJNative(asset))[0]?.amount ??
baseAmount(0, DOJ_DECIMAL);
const assetBalance: BaseAmount =
balances.filter(
({ asset: assetInList }) =>
assetToString(assetInList) === assetToString(asset)
)[0]?.amount ?? baseAmount(0, DOJ_DECIMAL);
const { average: fee } = await this.getFees();
if (isAssetDOJNative(asset)) {
// amount + fee < dojBalance
if (dojBalance.lt(amount.plus(fee))) {
throw new Error("insufficient funds");
}
} else {
// amount < assetBalances && dojBalance < fee
if (assetBalance.lt(amount) || dojBalance.lt(fee)) {
throw new Error("insufficient funds");
}
}
const privKey = this.getPrivateKey(walletIndex);
const signerPubkey = privKey.pubKey();
const fromAddress = this.getAddress(walletIndex);
const fromAddressAcc = cosmosclient.AccAddress.fromString(fromAddress);
const depositTxBody = await buildDepositTx({
msgNativeTx: {
memo: memo,
signer: fromAddressAcc,
coins: [
{
asset: asset,
amount: amount.amount().toString(),
},
],
},
nodeUrl: this.getClientUrl().node,
chainId: this.getChainId(),
});
const account = await this.getCosmosClient().getAccount(fromAddressAcc);
const { account_number: accountNumber } = account;
if (!accountNumber)
throw Error(
`Deposit failed - could not get account number ${accountNumber}`
);
const txBuilder = buildUnsignedTx({
cosmosSdk: this.getCosmosClient().sdk,
txBody: depositTxBody,
signerPubkey: cosmosclient.codec.instanceToProtoAny(signerPubkey),
gasLimit: Long.fromString(gasLimit.toFixed(0)),
sequence: account.sequence || Long.ZERO,
});
const txHash = await this.getCosmosClient().signAndBroadcast(
txBuilder,
privKey,
accountNumber
);
if (!txHash) throw Error(`Invalid transaction hash: ${txHash}`);
return txHash;
}
/**
* Transfer balances with MsgSend
*
* @param {TxParams} params The transfer options.
* @returns {TxHash} The transaction hash.
*
* @throws {"insufficient funds"} Thrown if the wallet has insufficient funds.
* @throws {"Invalid transaction hash"} Thrown by missing tx hash
*/
async transfer({
walletIndex = 0,
asset = AssetDOJNative,
amount,
recipient,
memo,
gasLimit = new BigNumber(DEFAULT_GAS_LIMIT_VALUE),
}: TxParams & { gasLimit?: BigNumber }): Promise<TxHash> {
const balances = await this.getBalance(this.getAddress(walletIndex), [
asset,
]);
const dojBalance: BaseAmount =
balances.filter(({ asset }) => isAssetDOJNative(asset))[0]?.amount ??
baseAmount(0, DOJ_DECIMAL);
const assetBalance: BaseAmount =
balances.filter(
({ asset: assetInList }) =>
assetToString(assetInList) === assetToString(asset)
)[0]?.amount ?? baseAmount(0, DOJ_DECIMAL);
const fee = (await this.getFees()).average;
if (isAssetDOJNative(asset)) {
// amount + fee < dojBalance
if (dojBalance.lt(amount.plus(fee))) {
throw new Error("insufficient funds");
}
} else {
// amount < assetBalances && dojBalance < fee
if (assetBalance.lt(amount) || dojBalance.lt(fee)) {
throw new Error("insufficient funds");
}
}
const privKey = this.getPrivateKey(walletIndex);
const from = this.getAddress(walletIndex);
const signerPubkey = privKey.pubKey();
const accAddress = cosmosclient.AccAddress.fromString(from);
const denom = getDenom(asset);
const txBody = await buildTransferTx({
fromAddress: from,
toAddress: recipient,
memo: memo,
assetAmount: amount,
assetDenom: denom,
chainId: this.getChainId(),
nodeUrl: this.getClientUrl().node,
});
const account = await this.getCosmosClient().getAccount(accAddress);
const { account_number: accountNumber } = account;
if (!accountNumber)
throw Error(
`Deposit failed - could not get account number ${accountNumber}`
);
const txBuilder = buildUnsignedTx({
cosmosSdk: this.getCosmosClient().sdk,
txBody: txBody,
gasLimit: Long.fromString(gasLimit.toString()),
signerPubkey: cosmosclient.codec.instanceToProtoAny(signerPubkey),
sequence: account.sequence || Long.ZERO,
});
const txHash = await this.cosmosClient.signAndBroadcast(
txBuilder,
privKey,
accountNumber
);
if (!txHash) throw Error(`Invalid transaction hash: ${txHash}`);
return txHash;
}
/**
* Transfer without broadcast balances with MsgSend
*
* @param {TxOfflineParams} params The transfer offline options.
* @returns {string} The signed transaction bytes.
*/
async transferOffline({
walletIndex = 0,
asset = AssetDOJNative,
amount,
recipient,
memo,
fromDojBalance: from_doj_balance,
fromAssetBalance: from_asset_balance = baseAmount(0, DOJ_DECIMAL),
fromAccountNumber = Long.ZERO,
fromSequence = Long.ZERO,
gasLimit = new BigNumber(DEFAULT_GAS_LIMIT_VALUE),
}: TxOfflineParams): Promise<string> {
const fee = (await this.getFees()).average;
if (isAssetDOJNative(asset)) {
// amount + fee < dojBalance
if (from_doj_balance.lt(amount.plus(fee))) {
throw new Error("insufficient funds");
}
} else {
// amount < assetBalances && dojBalance < fee
if (from_asset_balance.lt(amount) || from_doj_balance.lt(fee)) {
throw new Error("insufficient funds");
}
}
const txBody = await buildTransferTx({
fromAddress: this.getAddress(walletIndex),
toAddress: recipient,
memo,
assetAmount: amount,
assetDenom: getDenom(asset),
chainId: this.getChainId(),
nodeUrl: this.getClientUrl().node,
});
const privKey = this.getPrivateKey(walletIndex);
const txBuilder = buildUnsignedTx({
cosmosSdk: this.getCosmosClient().sdk,
txBody: txBody,
gasLimit: Long.fromString(gasLimit.toFixed(0)),
signerPubkey: cosmosclient.codec.instanceToProtoAny(privKey.pubKey()),
sequence: fromSequence,
});
const signDocBytes = txBuilder.signDocBytes(fromAccountNumber);
txBuilder.addSignature(privKey.sign(signDocBytes));
return txBuilder.txBytes();
}
/**
* Transaction with MsgSetNodePubkeysTx.
*
* @param {NodePubkeyParam} params The transaction options.
* @returns {TxHash} The transaction hash.
*
* @throws {"insufficient funds"} Thrown if the wallet has insufficient funds.
* @throws {"Invalid transaction hash"} Thrown by missing tx hash
*/
async setPubkeys({
walletIndex = 0,
secp256k1Pubkey,
ed25519Pubkey,
validatorConsPubkey,
gasLimit = new BigNumber(DEPOSIT_GAS_LIMIT_VALUE),
}: NodePubkeyParam): Promise<TxHash> {
const privKey = this.getPrivateKey(walletIndex);
const signerPubkey = privKey.pubKey();
const fromAddress = this.getAddress(walletIndex);
const fromAddressAcc = cosmosclient.AccAddress.fromString(fromAddress);
const setVersionTxBody = await buildSetPubkeysTx({
msgSetNodePubkeysTx: {
secp256k1Pubkey,
ed25519Pubkey,
validatorConsPubkey,
signer: fromAddressAcc,
},
nodeUrl: this.getClientUrl().node,
chainId: this.getChainId(),
});
const account = await this.getCosmosClient().getAccount(fromAddressAcc);
const { account_number: accountNumber } = account;
if (!accountNumber)
throw Error(
`Deposit failed - could not get account number ${accountNumber}`
);
const txBuilder = buildUnsignedTx({
cosmosSdk: this.getCosmosClient().sdk,
txBody: setVersionTxBody,
signerPubkey: cosmosclient.codec.instanceToProtoAny(signerPubkey),
sequence: account.sequence || Long.ZERO,
gasLimit: Long.fromString(gasLimit.toFixed(0)),
});
const txHash = await this.getCosmosClient().signAndBroadcast(
txBuilder,
privKey,
accountNumber
);
if (!txHash) throw Error(`Invalid transaction hash: ${txHash}`);
return txHash;
}
/**
* Transaction with MsgSetVersionTx.
*
* @param {VersionParam} params The transaction options.
* @returns {TxHash} The transaction hash.
*
* @throws {"insufficient funds"} Thrown if the wallet has insufficient funds.
* @throws {"Invalid transaction hash"} Thrown by missing tx hash
*/
async setVersion({
walletIndex = 0,
version,
gasLimit = new BigNumber(DEPOSIT_GAS_LIMIT_VALUE),
}: VersionParam): Promise<TxHash> {
const privKey = this.getPrivateKey(walletIndex);
const signerPubkey = privKey.pubKey();
const fromAddress = this.getAddress(walletIndex);
const fromAddressAcc = cosmosclient.AccAddress.fromString(fromAddress);
const setVersionTxBody = await buildSetVersionTx({
msgSetVersionTx: {
signer: fromAddressAcc,
version: version,
},
nodeUrl: this.getClientUrl().node,
chainId: this.getChainId(),
});
const account = await this.getCosmosClient().getAccount(fromAddressAcc);
const { account_number: accountNumber } = account;
if (!accountNumber)
throw Error(
`Deposit failed - could not get account number ${accountNumber}`
);
const txBuilder = buildUnsignedTx({
cosmosSdk: this.getCosmosClient().sdk,
txBody: setVersionTxBody,
signerPubkey: cosmosclient.codec.instanceToProtoAny(signerPubkey),
sequence: account.sequence || Long.ZERO,
gasLimit: Long.fromString(gasLimit.toFixed(0)),
});
const txHash = await this.getCosmosClient().signAndBroadcast(
txBuilder,
privKey,
accountNumber
);
if (!txHash) throw Error(`Invalid transaction hash: ${txHash}`);
return txHash;
}
/**
* Transaction with MsgSetIpAddressTx.
*
* @param {IpAddressParam} params The transaction options.
* @returns {TxHash} The transaction hash.
*
* @throws {"insufficient funds"} Thrown if the wallet has insufficient funds.
* @throws {"Invalid transaction hash"} Thrown by missing tx hash
*/
async setIpAddress({
walletIndex = 0,
ipAddress,
gasLimit = new BigNumber(DEPOSIT_GAS_LIMIT_VALUE),
}: IpAddressParam): Promise<TxHash> {
const privKey = this.getPrivateKey(walletIndex);
const signerPubkey = privKey.pubKey();
const fromAddress = this.getAddress(walletIndex);
const fromAddressAcc = cosmosclient.AccAddress.fromString(fromAddress);
const setIpAddressTxBody = await buildSetIpAddressTx({
msgSetIpAddressTx: {
signer: fromAddressAcc,
ipAddress: ipAddress,
},
nodeUrl: this.getClientUrl().node,
chainId: this.getChainId(),
});
const account = await this.getCosmosClient().getAccount(fromAddressAcc);
const { account_number: accountNumber } = account;
if (!accountNumber)
throw Error(
`Deposit failed - could not get account number ${accountNumber}`
);
const txBuilder = buildUnsignedTx({
cosmosSdk: this.getCosmosClient().sdk,
txBody: setIpAddressTxBody,
signerPubkey: cosmosclient.codec.instanceToProtoAny(signerPubkey),
sequence: account.sequence || Long.ZERO,
gasLimit: Long.fromString(gasLimit.toFixed(0)),
});
const txHash = await this.getCosmosClient().signAndBroadcast(
txBuilder,
privKey,
accountNumber
);
if (!txHash) throw Error(`Invalid transaction hash: ${txHash}`);
return txHash;
}
/**
* Gets fees from Node
*
* @returns {Fees}
*/
async getFees(): Promise<Fees> {
try {
const {
data: {
int_64_values: { NativeTransactionFee: fee },
},
} = await axios.get<HermeschainConstantsResponse>(
`${this.getClientUrl().node}/hermeschain/constants`
);
// validate data
if (!fee || isNaN(fee) || fee < 0)
throw Error(`Invalid fee: ${fee.toString()}`);
return singleFee(FeeType.FlatFee, baseAmount(fee));
} catch {
return getDefaultFees();
}
}
getSwapOutput(inputAmount: number, pool: PoolData, toDoj: boolean): number {
const input = inputAmount * Math.pow(10, DOJ_DECIMAL);
return calcSwapOutput(input, pool, toDoj);
}
getDoubleSwapOutput(
inputAmount: number,
pool1: PoolData,
pool2: PoolData
): number {
const input = inputAmount * Math.pow(10, DOJ_DECIMAL);
return calcDoubleSwapOutput(input, pool1, pool2);
}
getSwapSlip(inputAmount: number, pool: PoolData, toDoj: boolean): number {
const input = inputAmount * Math.pow(10, DOJ_DECIMAL);
return calcSwapSlip(input, pool, toDoj);
}
getDoubleSwapSlip(
inputAmount: number,
pool1: PoolData,
pool2: PoolData
): number {
const input = inputAmount * Math.pow(10, DOJ_DECIMAL);
return calcDoubleSwapSlip(input, pool1, pool2);
}
async getSwapFeesData(): Promise<SwapFeeResult> {
return;
}
}
export { HermesClient };