diff --git a/package.json b/package.json
index dfca8ee2d3..ab84ad0a78 100644
--- a/package.json
+++ b/package.json
@@ -23,8 +23,8 @@
"test:scripts": "jest -i --verbose ./test-scripts/*.test.ts"
},
"dependencies": {
- "@cosmjs/proto-signing": "^0.28.10",
- "@cosmjs/stargate": "^0.28.13",
+ "@cosmjs/proto-signing": "^0.30.1",
+ "@cosmjs/stargate": "^0.30.1",
"@crocswap/sdk": "^2.4.5",
"@ethersproject/abstract-provider": "5.7.0",
"@ethersproject/address": "5.7.0",
@@ -77,7 +77,10 @@
"express": "^4.17.1",
"express-winston": "^4.1.0",
"fs-extra": "^10.0.0",
+ "http-status-codes": "2.2.0",
+ "immutable": "^4.2.4",
"js-yaml": "^4.1.0",
+ "kujira.js": "https://github.com/Team-Kujira/kujira.js#911a8acbfa28909cc0ad31a5007ee23fc8fd7c5a",
"level": "^8.0.0",
"lodash": "^4.17.21",
"lru-cache": "^7.14.1",
@@ -139,7 +142,6 @@
"jest-extended": "^0.11.5",
"jsbi": "^3.2.0",
"mock-ethers-provider": "^1.0.2",
- "node-cache": "5.1.2",
"nodemon": "^2.0.16",
"prettier": "^2.3.0",
"react": "^18",
diff --git a/src/chains/kujira/kujira.ts b/src/chains/kujira/kujira.ts
new file mode 100644
index 0000000000..021828698a
--- /dev/null
+++ b/src/chains/kujira/kujira.ts
@@ -0,0 +1,231 @@
+import { KujiraModel } from '../../connectors/kujira/kujira.model';
+import { convertToGetTokensResponse } from '../../connectors/kujira/kujira.convertors';
+import { KujiraConfig } from '../../connectors/kujira/kujira.config';
+import {
+ Address,
+ GetCurrentBlockRequest,
+ GetCurrentBlockResponse,
+ Token,
+} from '../../connectors/kujira/kujira.types';
+import { TokenInfo } from '../ethereum/ethereum-base';
+import {
+ BalanceRequest,
+ PollRequest,
+ TokensRequest,
+ TokensResponse,
+} from '../../network/network.requests';
+import { Chain, CustomTransaction } from '../../services/common-interfaces';
+import {
+ AllowancesRequest,
+ ApproveRequest,
+ CancelRequest,
+ NonceRequest,
+ NonceResponse,
+} from '../chain.requests';
+import {
+ TransferRequest,
+ TransferResponse,
+} from '../injective/injective.requests';
+import { BigNumber } from 'bignumber.js';
+
+export class Kujira {
+ chain: string = 'kujira';
+ network: string;
+ controller: Kujira = this;
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ private kujira: KujiraModel;
+ storedTokenList: any;
+
+ private static _instances: { [name: string]: Kujira };
+
+ private constructor(network: string) {
+ this.network = network;
+ }
+
+ public static getInstance(chain: string): Kujira {
+ if (Kujira._instances === undefined) {
+ Kujira._instances = {};
+ }
+
+ const key = `${chain}`;
+
+ if (!(key in Kujira._instances)) {
+ Kujira._instances[key] = new Kujira(chain);
+ }
+
+ return Kujira._instances[key];
+ }
+
+ public static getConnectedInstances(): { [key: string]: Kujira } {
+ return Kujira._instances;
+ }
+
+ async init() {
+ this.kujira = KujiraModel.getInstance(this.chain, this.network);
+ await this.kujira.init();
+ }
+
+ ready(): boolean {
+ return this.kujira ? this.kujira.isReady : false;
+ }
+
+ async getWalletPublicKey(
+ mnemonic: string,
+ accountNumber: number | undefined
+ ): Promise
{
+ return await this.kujira.getWalletPublicKey({
+ mnemonic: mnemonic,
+ accountNumber: accountNumber || KujiraConfig.config.accountNumber,
+ });
+ }
+
+ async encrypt(
+ mnemonic: string,
+ accountNumber: number,
+ publicKey: string
+ ): Promise {
+ return await this.kujira.encryptWallet({
+ wallet: {
+ mnemonic,
+ accountNumber,
+ publicKey,
+ },
+ });
+ }
+
+ async getTokenForSymbol(symbol: string): Promise {
+ return convertToGetTokensResponse(await this.kujira.getToken({ symbol }));
+ }
+
+ async getCurrentBlockNumber(
+ _options: GetCurrentBlockRequest
+ ): Promise {
+ return await this.kujira.getCurrentBlock(_options);
+ }
+
+ async balances(
+ _chain: any,
+ req: BalanceRequest
+ ): Promise<{ balances: Record }> {
+ let balances;
+ if (req.tokenSymbols && req.tokenSymbols.length) {
+ balances = await this.kujira.getBalances({
+ ownerAddress: req.address,
+ tokenSymbols: req.tokenSymbols,
+ });
+ } else {
+ balances = await this.kujira.getAllBalances({
+ ownerAddress: req.address,
+ });
+ }
+
+ const output: Record = {};
+
+ for (const balance of balances.tokens.values()) {
+ output[(balance.token as Token).symbol] = balance.free.toString();
+ }
+
+ return { balances: output };
+ }
+
+ async poll(_chain: Chain, req: PollRequest): Promise {
+ const currentBlock = await this.kujira.getCurrentBlock({});
+
+ const transaction = await this.kujira.getTransaction({
+ hash: req.txHash,
+ });
+
+ // noinspection UnnecessaryLocalVariableJS
+ const output = {
+ currentBlock: currentBlock,
+ txHash: transaction.hash,
+ txStatus: transaction.code,
+ txBlock: transaction.blockNumber,
+ txData: transaction.data,
+ txReceipt: undefined,
+ };
+
+ return output;
+ }
+
+ async getTokens(_chain: Chain, _req: TokensRequest): Promise {
+ const tokens = await this.kujira.getAllTokens({});
+
+ const output: {
+ tokens: any[];
+ } = {
+ tokens: [],
+ };
+
+ for (const token of tokens.values()) {
+ output.tokens.push({
+ chainId: this.kujira.chain,
+ address: token.id,
+ name: token.name,
+ symbol: token.symbol,
+ decimals: token.decimals,
+ });
+ }
+
+ return output;
+ }
+
+ async nextNonce(_chain: Chain, _req: NonceRequest): Promise {
+ // Not applicable.
+
+ return {
+ nonce: undefined as unknown as number,
+ };
+ }
+
+ async nonce(_chain: Chain, _req: NonceRequest): Promise {
+ // Not applicable.
+
+ return {
+ nonce: undefined as unknown as number,
+ };
+ }
+
+ async allowances(_chain: Chain, _req: AllowancesRequest): Promise {
+ // Not applicable.
+
+ return {
+ spender: undefined as unknown as string,
+ approvals: {} as Record,
+ };
+ }
+
+ async approve(_chain: Chain, _req: ApproveRequest): Promise {
+ // Not applicable.
+
+ return {
+ tokenAddress: undefined as unknown as string,
+ spender: undefined as unknown as string,
+ amount: undefined as unknown as string,
+ nonce: undefined as unknown as number,
+ approval: undefined as unknown as CustomTransaction,
+ };
+ }
+
+ async cancel(_chain: Chain, _req: CancelRequest): Promise {
+ // Not applicable.
+
+ return {
+ txHash: undefined as unknown as string,
+ };
+ }
+
+ async transfer(
+ _chain: Chain,
+ req: TransferRequest
+ ): Promise {
+ return this.kujira.transferFromTo({
+ from: req.from,
+ to: req.to,
+ tokenSymbol: req.token,
+ amount: BigNumber(req.amount),
+ });
+ }
+}
diff --git a/src/clob/clob.validators.ts b/src/clob/clob.validators.ts
index 84d60b5158..9fbe58d178 100644
--- a/src/clob/clob.validators.ts
+++ b/src/clob/clob.validators.ts
@@ -17,6 +17,7 @@ import {
validateAmount,
validateSide,
} from '../amm/amm.validators';
+import { isValidKujiraPublicKey } from '../connectors/kujira/kujira.helpers';
export const invalidMarketError: string =
'The market param is not a valid market. Market should be in {base}-{quote} format.';
@@ -95,7 +96,10 @@ export const validateWallet: Validator = mkValidator(
'address',
invalidWalletError,
(val) => {
- return typeof val === 'string' && isAddress(val.slice(0, 42));
+ return (
+ typeof val === 'string' &&
+ (isAddress(val.slice(0, 42)) || isValidKujiraPublicKey(val))
+ );
}
);
@@ -143,7 +147,7 @@ export const validatePostPerpOrderRequest: RequestValidator =
);
export const validateOrderRequest: RequestValidator = mkRequestValidator(
- NETWORK_VALIDATIONS.concat([validateOrderId])
+ NETWORK_VALIDATIONS.concat([])
);
export const validatePerpTradesRequest: RequestValidator = mkRequestValidator(
diff --git a/src/connectors/connectors.routes.ts b/src/connectors/connectors.routes.ts
index 178792e01e..b22bf8f20b 100644
--- a/src/connectors/connectors.routes.ts
+++ b/src/connectors/connectors.routes.ts
@@ -20,6 +20,7 @@ import { ConnectorsResponse } from './connectors.request';
import { DexalotCLOBConfig } from './dexalot/dexalot.clob.config';
import { TinymanConfig } from './tinyman/tinyman.config';
import { PlentyConfig } from './plenty/plenty.config';
+import { KujiraConfig } from './kujira/kujira.config';
export namespace ConnectorsRoutes {
export const router = Router();
@@ -157,7 +158,17 @@ export namespace ConnectorsRoutes {
trading_type: PlentyConfig.config.tradingTypes,
chain_type: PlentyConfig.config.chainType,
available_networks: PlentyConfig.config.availableNetworks,
- }
+ },
+ {
+ name: 'kujira',
+ trading_type: KujiraConfig.config.tradingTypes,
+ chain_type: KujiraConfig.config.chainType,
+ available_networks: KujiraConfig.config.availableNetworks,
+ additional_add_wallet_prompts: {
+ accountId:
+ 'Enter your kujira account number (input 0 if unsure) >>> ',
+ },
+ },
],
});
})
diff --git a/src/connectors/kujira/kujira.config.ts b/src/connectors/kujira/kujira.config.ts
new file mode 100644
index 0000000000..0e736906d6
--- /dev/null
+++ b/src/connectors/kujira/kujira.config.ts
@@ -0,0 +1,105 @@
+import { BigNumber } from 'bignumber.js';
+import { ConfigManagerV2 } from '../../services/config-manager-v2';
+
+const configManager = ConfigManagerV2.getInstance();
+
+export interface NetworkConfig {
+ name: string;
+ nodeURL: string | null;
+ chainId: string;
+ tokenListType: string;
+ tokenListSource: string;
+}
+
+export namespace KujiraConfig {
+ export const config = {
+ chainType: 'KUJIRA',
+ tradingTypes: ['CLOB_SPOT'],
+ chain: 'kujira',
+ networks: new Map(
+ Object.entries(configManager.get(`kujira.networks`))
+ ),
+ availableNetworks: [
+ {
+ chain: 'kujira',
+ networks: Object.keys(configManager.get(`kujira.networks`)),
+ },
+ ],
+ connector: 'kujira',
+ prefix: configManager.get('kujira.prefix') || 'kujira',
+ accountNumber: configManager.get('kujira.accountNumber') || 0,
+ nativeToken: 'KUJI',
+ gasPrice: BigNumber(configManager.get('kujira.gasPrice') || 0.00125),
+ gasPriceSuffix: 'ukuji',
+ gasLimitEstimate: BigNumber(
+ configManager.get('kujira.gasLimitEstimate') || 0.009147
+ ),
+ tokens: {
+ disallowed: configManager.get(`kujira.tokens.disallowed`),
+ allowed: configManager.get(`kujira.tokens.allowed`),
+ },
+ markets: {
+ disallowed: configManager.get(`kujira.markets.disallowed`),
+ allowed: configManager.get(`kujira.markets.allowed`),
+ },
+ fees: {
+ maker: BigNumber(0.075), // Percentual value (%)
+ taker: BigNumber(0.15), // Percentual value (%)
+ serviceProvider: BigNumber(0), // Percentual value (%)
+ },
+ orders: {
+ create: {
+ fee: configManager.get(`kujira.orders.create.fee`),
+ maxPerTransaction: configManager.get(
+ `kujira.orders.create.maxPerTransaction`
+ ),
+ },
+ open: {
+ limit: configManager.get(`kujira.orders.open.limit`) | 255,
+ paginationLimit:
+ configManager.get(`kujira.orders.open.paginationLimit`) | 31,
+ },
+ filled: {
+ limit: configManager.get(`kujira.orders.filled.limit`) | 255,
+ },
+ cancel: {
+ maxPerTransaction: configManager.get(
+ `kujira.orders.cancel.maxPerTransaction`
+ ),
+ },
+ },
+ transactions: {
+ merge: {
+ createOrders: configManager.get(
+ `kujira.transactions.merge.createOrders`
+ ),
+ cancelOrders: configManager.get(
+ `kujira.transactions.merge.cancelOrders`
+ ),
+ settleFunds: configManager.get(`kujira.transactions.merge.settleFunds`),
+ },
+ },
+ orderBook: {
+ offset: configManager.get(`kujira.orderBook.offset`) || 0,
+ limit: configManager.get(`kujira.orderBook.limit`) || 255,
+ },
+ retry: {
+ all: {
+ maxNumberOfRetries:
+ configManager.get('kujira.retry.all.maxNumberOfRetries') || 0, // 0 means no retries
+ delayBetweenRetries:
+ configManager.get('kujira.retry.all.delayBetweenRetries') || 0, // 0 means no delay (milliseconds)
+ },
+ },
+ timeout: {
+ all: configManager.get('kujira.timeout.all') || 0, // 0 means no timeout (milliseconds)
+ },
+ parallel: {
+ all: {
+ batchSize: configManager.get('kujira.parallel.all.batchSize') || 0, // 0 means no batching (group all)
+ delayBetweenBatches:
+ configManager.get('kujira.parallel.all.delayBetweenBatches') || 0, // 0 means no delay (milliseconds)
+ },
+ },
+ };
+}
diff --git a/src/connectors/kujira/kujira.controllers.ts b/src/connectors/kujira/kujira.controllers.ts
new file mode 100644
index 0000000000..16755a5ebb
--- /dev/null
+++ b/src/connectors/kujira/kujira.controllers.ts
@@ -0,0 +1,838 @@
+import { StatusCodes } from 'http-status-codes';
+import { ResponseWrapper } from '../../services/common-interfaces';
+import { HttpException } from '../../services/error-handler';
+import { KujiraModel as Connector } from './kujira.model';
+import { convertToResponseBody } from './kujira.convertors';
+import {
+ AllMarketsWithdrawsRequest,
+ AllMarketsWithdrawsResponse,
+ BalanceNotFoundError,
+ CancelAllOrdersRequest,
+ CancelAllOrdersResponse,
+ CancelOrderRequest,
+ CancelOrderResponse,
+ CancelOrdersRequest,
+ CancelOrdersResponse,
+ GetAllBalancesRequest,
+ GetAllBalancesResponse,
+ GetAllMarketsRequest,
+ GetAllMarketsResponse,
+ GetAllOrderBooksRequest,
+ GetAllOrderBooksResponse,
+ GetAllTickersRequest,
+ GetAllTickersResponse,
+ GetAllTokensRequest,
+ GetAllTokensResponse,
+ GetBalanceRequest,
+ GetBalanceResponse,
+ GetBalancesRequest,
+ GetBalancesResponse,
+ GetCurrentBlockRequest,
+ GetCurrentBlockResponse,
+ GetEstimatedFeesRequest,
+ GetEstimatedFeesResponse,
+ GetMarketRequest,
+ GetMarketResponse,
+ GetMarketsRequest,
+ GetMarketsResponse,
+ GetOrderBookRequest,
+ GetOrderBookResponse,
+ GetOrderBooksRequest,
+ GetOrderBooksResponse,
+ GetOrderRequest,
+ GetOrderResponse,
+ GetOrdersRequest,
+ GetOrdersResponse,
+ GetRootRequest,
+ GetRootResponse,
+ GetTickerRequest,
+ GetTickerResponse,
+ GetTickersRequest,
+ GetTickersResponse,
+ GetTokenRequest,
+ GetTokenResponse,
+ GetTokensRequest,
+ GetTokensResponse,
+ GetTransactionRequest,
+ GetTransactionResponse,
+ GetTransactionsRequest,
+ GetTransactionsResponse,
+ GetWalletPublicKeyRequest,
+ GetWalletPublicKeyResponse,
+ GetWalletsPublicKeysRequest,
+ GetWalletsPublicKeysResponse,
+ MarketNotFoundError,
+ MarketsWithdrawsFundsResponse,
+ MarketsWithdrawsRequest,
+ MarketWithdrawRequest,
+ MarketWithdrawResponse,
+ OrderBookNotFoundError,
+ OrderNotFoundError,
+ PlaceOrderRequest,
+ PlaceOrderResponse,
+ PlaceOrdersRequest,
+ PlaceOrdersResponse,
+ TickerNotFoundError,
+ TokenNotFoundError,
+ TransactionNotFoundError,
+ WalletPublicKeyNotFoundError,
+} from './kujira.types';
+import {
+ validateCancelAllOrdersRequest,
+ validateCancelOrderRequest,
+ validateCancelOrdersRequest,
+ validateGetAllBalancesRequest,
+ validateGetAllMarketsRequest,
+ validateGetAllOrderBooksRequest,
+ validateGetAllOrdersRequest,
+ validateGetAllTickersRequest,
+ validateGetAllTokensRequest,
+ validateGetBalanceRequest,
+ validateGetBalancesRequest,
+ validateGetCurrentBlockRequest,
+ validateGetEstimatedFeesRequest,
+ validateGetMarketRequest,
+ validateGetMarketsRequest,
+ validateGetOrderBookRequest,
+ validateGetOrderBooksRequest,
+ validateGetOrderRequest,
+ validateGetOrdersRequest,
+ validateGetTickerRequest,
+ validateGetTickersRequest,
+ validateGetTokenRequest,
+ validateGetTokensRequest,
+ validateGetTransactionRequest,
+ validateGetTransactionsRequest,
+ validateGetWalletPublicKeyRequest,
+ validateGetWalletsPublicKeysRequest,
+ validatePlaceOrderRequest,
+ validatePlaceOrdersRequest,
+ validateSettleAllMarketsFundsRequest,
+ validateSettleMarketFundsRequest,
+ validateSettleMarketsFundsRequest,
+} from './kujira.validators';
+
+export async function getRoot(
+ connector: Connector,
+ request: GetRootRequest
+): Promise> {
+ const response = new ResponseWrapper();
+
+ response.body = convertToResponseBody(await connector.getRoot(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+}
+
+export async function getToken(
+ connector: Connector,
+ request: GetTokenRequest
+): Promise> {
+ validateGetTokenRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.getToken(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof TokenNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getTokens(
+ connector: Connector,
+ request: GetTokensRequest
+): Promise> {
+ validateGetTokensRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.getTokens(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof TokenNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getAllTokens(
+ connector: Connector,
+ request: GetAllTokensRequest
+): Promise> {
+ validateGetAllTokensRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.getAllTokens(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof TokenNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getMarket(
+ connector: Connector,
+ request: GetMarketRequest
+): Promise> {
+ validateGetMarketRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.getMarket(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof MarketNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getMarkets(
+ connector: Connector,
+ request: GetMarketsRequest
+): Promise> {
+ validateGetMarketsRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.getMarkets(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof MarketNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getAllMarkets(
+ connector: Connector,
+ request: GetAllMarketsRequest
+): Promise> {
+ validateGetAllMarketsRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.getAllMarkets(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof MarketNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getOrderBook(
+ connector: Connector,
+ request: GetOrderBookRequest
+): Promise> {
+ validateGetOrderBookRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.getOrderBook(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof OrderBookNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getOrderBooks(
+ connector: Connector,
+ request: GetOrderBooksRequest
+): Promise> {
+ validateGetOrderBooksRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.getOrderBooks(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof OrderBookNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getAllOrderBooks(
+ connector: Connector,
+ request: GetAllOrderBooksRequest
+): Promise> {
+ validateGetAllOrderBooksRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.getAllOrderBooks(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof OrderBookNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getTicker(
+ connector: Connector,
+ request: GetTickerRequest
+): Promise> {
+ validateGetTickerRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.getTicker(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof TickerNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getTickers(
+ connector: Connector,
+ request: GetTickersRequest
+): Promise> {
+ validateGetTickersRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.getTickers(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof TickerNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getAllTickers(
+ connector: Connector,
+ request: GetAllTickersRequest
+): Promise> {
+ validateGetAllTickersRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.getAllTickers(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof TickerNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getBalance(
+ connector: Connector,
+ request: GetBalanceRequest
+): Promise> {
+ validateGetBalanceRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.getBalance(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof BalanceNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getBalances(
+ connector: Connector,
+ request: GetBalancesRequest
+): Promise> {
+ validateGetBalancesRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.getBalances(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof BalanceNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getAllBalances(
+ connector: Connector,
+ request: GetAllBalancesRequest
+): Promise> {
+ validateGetAllBalancesRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.getAllBalances(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof BalanceNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getOrder(
+ connector: Connector,
+ request: GetOrderRequest
+): Promise> {
+ validateGetOrderRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.getOrder(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof OrderNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getOrders(
+ connector: Connector,
+ request: GetOrdersRequest
+): Promise> {
+ if (request.ids) {
+ validateGetOrdersRequest(request);
+ } else if (
+ request.marketId ||
+ request.marketIds ||
+ request.marketName ||
+ request.marketNames ||
+ request.status ||
+ request.statuses
+ ) {
+ validateGetAllOrdersRequest(request);
+ }
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.getOrders(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof OrderNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function placeOrder(
+ connector: Connector,
+ request: PlaceOrderRequest
+): Promise> {
+ validatePlaceOrderRequest(request);
+
+ const response = new ResponseWrapper();
+
+ response.body = convertToResponseBody(await connector.placeOrder(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+}
+
+export async function placeOrders(
+ connector: Connector,
+ request: PlaceOrdersRequest
+): Promise> {
+ validatePlaceOrdersRequest(request);
+
+ const response = new ResponseWrapper();
+
+ response.body = convertToResponseBody(await connector.placeOrders(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+}
+
+export async function cancelOrder(
+ connector: Connector,
+ request: CancelOrderRequest
+): Promise> {
+ validateCancelOrderRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(await connector.cancelOrder(request));
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof OrderNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function cancelOrders(
+ connector: Connector,
+ request: CancelOrdersRequest
+): Promise> {
+ validateCancelOrdersRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.cancelOrders(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof OrderNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function cancelAllOrders(
+ connector: Connector,
+ request: CancelAllOrdersRequest
+): Promise> {
+ validateCancelAllOrdersRequest(request);
+
+ const response = new ResponseWrapper();
+
+ response.body = convertToResponseBody(
+ await connector.cancelAllOrders(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+}
+
+export async function withdrawFromMarket(
+ connector: Connector,
+ request: MarketWithdrawRequest
+): Promise> {
+ validateSettleMarketFundsRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.withdrawFromMarket(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof MarketNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function withdrawFromMarkets(
+ connector: Connector,
+ request: MarketsWithdrawsRequest
+): Promise> {
+ validateSettleMarketsFundsRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.withdrawFromMarkets(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof MarketNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function withdrawFromAllMarkets(
+ connector: Connector,
+ request: AllMarketsWithdrawsRequest
+): Promise> {
+ validateSettleAllMarketsFundsRequest(request);
+
+ const response = new ResponseWrapper();
+
+ response.body = convertToResponseBody(
+ await connector.withdrawFromAllMarkets(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+}
+
+export async function getWalletPublicKey(
+ connector: Connector,
+ request: GetWalletPublicKeyRequest
+): Promise> {
+ validateGetWalletPublicKeyRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.getWalletPublicKey(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof WalletPublicKeyNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getWalletsPublicKeys(
+ connector: Connector,
+ request: GetWalletsPublicKeysRequest
+): Promise> {
+ validateGetWalletsPublicKeysRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ connector.getWalletsPublicKeys(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof WalletPublicKeyNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getTransaction(
+ connector: Connector,
+ request: GetTransactionRequest
+): Promise> {
+ validateGetTransactionRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.getTransaction(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof TransactionNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getTransactions(
+ connector: Connector,
+ request: GetTransactionsRequest
+): Promise> {
+ validateGetTransactionsRequest(request);
+
+ const response = new ResponseWrapper();
+
+ try {
+ response.body = convertToResponseBody(
+ await connector.getTransactions(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+ } catch (exception) {
+ if (exception instanceof TransactionNotFoundError) {
+ throw new HttpException(StatusCodes.NOT_FOUND, exception.message);
+ } else {
+ throw exception;
+ }
+ }
+}
+
+export async function getCurrentBlock(
+ connector: Connector,
+ request: GetCurrentBlockRequest
+): Promise> {
+ validateGetCurrentBlockRequest(request);
+
+ const response = new ResponseWrapper();
+
+ response.body = convertToResponseBody(
+ await connector.getCurrentBlock(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+}
+
+export async function getEstimatedFees(
+ connector: Connector,
+ request: GetEstimatedFeesRequest
+): Promise> {
+ validateGetEstimatedFeesRequest(request);
+
+ const response = new ResponseWrapper();
+
+ response.body = convertToResponseBody(
+ await connector.getEstimatedFees(request)
+ );
+
+ response.status = StatusCodes.OK;
+
+ return response;
+}
diff --git a/src/connectors/kujira/kujira.convertors.ts b/src/connectors/kujira/kujira.convertors.ts
new file mode 100644
index 0000000000..f8a9566b30
--- /dev/null
+++ b/src/connectors/kujira/kujira.convertors.ts
@@ -0,0 +1,669 @@
+import {
+ Balance,
+ Balances,
+ ConvertOrderType,
+ IMap,
+ KujiraEvent,
+ KujiraOrderBook,
+ KujiraTicker,
+ KujiraWithdraw,
+ Market,
+ MarketName,
+ Order,
+ OrderAmount,
+ OrderBook,
+ OrderId,
+ OrderPrice,
+ OrderSide,
+ OrderStatus,
+ OrderType,
+ Ticker,
+ Token,
+ TokenId,
+ Transaction,
+ TransactionHashes,
+ Withdraw,
+} from './kujira.types';
+import { KujiraConfig } from './kujira.config';
+import {
+ Denom,
+ fin,
+ KUJI,
+ MAINNET,
+ NETWORKS,
+ TESTNET,
+ USK,
+ USK_TESTNET,
+} from 'kujira.js';
+import { IndexedTx } from '@cosmjs/stargate/build/stargateclient';
+import contracts from 'kujira.js/src/resources/contracts.json';
+import { getNotNullOrThrowError } from './kujira.helpers';
+import { BigNumber } from 'bignumber.js';
+import { Coin } from '@cosmjs/proto-signing';
+import { parseCoins } from '@cosmjs/stargate';
+import { TokenInfo } from '../../services/base';
+import { ClobDeleteOrderRequestExtract } from '../../clob/clob.requests';
+
+export const convertToGetTokensResponse = (token: Token): TokenInfo => {
+ return {
+ chainId: token.id,
+ address: undefined,
+ name: token.name,
+ symbol: token.symbol,
+ decimals: token.decimals,
+ } as unknown as TokenInfo;
+};
+
+export const convertKujiraTokenToToken = (token: Denom): Token => {
+ return {
+ id: token.reference,
+ name: token.symbol,
+ symbol: token.symbol,
+ decimals: token.decimals,
+ };
+};
+
+export const convertHumingbotMarketNameToMarketName = (
+ input: string
+): MarketName => {
+ return input.replace('-', '/');
+};
+
+export const convertMarketNameToHumingbotMarketName = (
+ input: string
+): string => {
+ return input.replace('/', '-');
+};
+
+export const convertKujiraMarketToMarket = (market: fin.Pair): Market => {
+ const baseToken = convertKujiraTokenToToken(market.denoms[0]);
+ const quoteToken = convertKujiraTokenToToken(market.denoms[1]);
+
+ const decimalPlaces =
+ 'decimal_places' in market.precision
+ ? market.precision?.decimal_places
+ : market.precision.significant_figures;
+
+ const minimumPriceIncrement = BigNumber(Math.pow(10, -1 * decimalPlaces));
+
+ return {
+ id: market.address,
+ name: `${baseToken.symbol}/${quoteToken.symbol}`,
+ baseToken: baseToken,
+ quoteToken: quoteToken,
+ precision: decimalPlaces,
+ minimumOrderSize: minimumPriceIncrement, // Considering the market precision as the minimum value
+ minimumPriceIncrement: minimumPriceIncrement, // Considering the market precision as the minimum value
+ minimumBaseAmountIncrement: minimumPriceIncrement, // Considering the market precision as the minimum value
+ minimumQuoteAmountIncrement: minimumPriceIncrement, // Considering the market precision as the minimum value
+ fees: {
+ maker: KujiraConfig.config.fees.maker,
+ taker: KujiraConfig.config.fees.taker,
+ serviceProvider: KujiraConfig.config.fees.serviceProvider,
+ },
+ programId: undefined,
+ deprecated: false,
+ connectorMarket: market,
+ } as Market;
+};
+
+export const convertKujiraOrderBookToOrderBook = (
+ market: Market,
+ kujiraOrderBook: KujiraOrderBook
+): OrderBook => {
+ const bids = IMap().asMutable();
+ const asks = IMap().asMutable();
+ let bestBid: Order | undefined;
+ let bestAsk: Order | undefined;
+ let bestBidPrice = BigNumber('-Infinity');
+ let bestAskPrice = BigNumber('Infinity');
+
+ let counter = 0;
+ kujiraOrderBook.base.forEach((kujiraOrder) => {
+ const order = {
+ id: undefined,
+ clientId: undefined,
+ marketName: market.name,
+ marketId: market.id,
+ ownerAddress: undefined,
+ payerAddress: undefined,
+ price: BigNumber(kujiraOrder.quote_price),
+ amount: BigNumber(kujiraOrder.total_offer_amount),
+ side: OrderSide.SELL,
+ status: OrderStatus.OPEN,
+ type: OrderType.LIMIT,
+ fee: undefined,
+ fillingTimestamp: undefined,
+ hashes: undefined,
+ connectorOrder: undefined,
+ } as Order;
+
+ if (bestAsk) {
+ if (order.price?.lt(bestAskPrice)) {
+ bestAsk = order;
+ bestAskPrice = getNotNullOrThrowError(order.price);
+ }
+ } else {
+ bestAsk = order;
+ bestAskPrice = getNotNullOrThrowError(order.price);
+ }
+
+ asks.set(`unknown_${counter++}`, order);
+ });
+
+ kujiraOrderBook.quote.forEach((kujiraOrder) => {
+ const order = {
+ id: undefined,
+ clientId: undefined,
+ marketName: market.name,
+ marketId: market.id,
+ ownerAddress: undefined,
+ payerAddress: undefined,
+ price: BigNumber(kujiraOrder.quote_price),
+ amount: BigNumber(kujiraOrder.total_offer_amount),
+ side: OrderSide.BUY,
+ status: OrderStatus.OPEN,
+ type: OrderType.LIMIT,
+ fee: undefined,
+ fillingTimestamp: undefined,
+ hashes: undefined,
+ connectorOrder: undefined,
+ } as Order;
+
+ if (bestBid) {
+ if (order.price?.gt(bestBidPrice)) {
+ bestBid = order;
+ bestBidPrice = getNotNullOrThrowError(order.price);
+ }
+ } else {
+ bestBid = order;
+ bestBidPrice = getNotNullOrThrowError(order.price);
+ }
+
+ bids.set(`unknown_${counter++}`, order);
+ });
+
+ return {
+ market: market,
+ bids: bids,
+ asks: asks,
+ bestBid: bestBid,
+ bestAsk: bestAsk,
+ connectorOrderBook: kujiraOrderBook,
+ } as OrderBook;
+};
+
+export const convertOfferDenomToOrderSide = (
+ offer_denom: string,
+ market: Market
+): OrderSide => {
+ const offerDenom = Denom.from(offer_denom);
+ const baseTokenDenom = Denom.from(market.baseToken.id);
+ const quoteTokenDenom = Denom.from(market.quoteToken.id);
+
+ if (offerDenom.eq(baseTokenDenom)) {
+ return OrderSide.SELL;
+ } else if (offerDenom.eq(quoteTokenDenom)) {
+ return OrderSide.BUY;
+ } else {
+ throw new Error('Order side from offer denom not recognized');
+ }
+};
+
+export const convertKujiraOrderToStatus = (kujiraOrder: any): OrderStatus => {
+ if (kujiraOrder['offer_amount'] == '0') {
+ return OrderStatus.FILLED;
+ } else if (
+ kujiraOrder['offer_amount'] == kujiraOrder['original_offer_amount']
+ ) {
+ return OrderStatus.OPEN;
+ } else {
+ return OrderStatus.PARTIALLY_FILLED;
+ }
+};
+
+export const convertKujiraFeeToFee = (kujiraFee: string) => {
+ const fee = parseCoins(kujiraFee)[0];
+ return BigNumber(fee.amount).multipliedBy(
+ BigNumber('1e-' + KUJI.decimals.toString())
+ );
+};
+
+export const convertKujiraOrdersToMapOfOrders = (options: {
+ type: ConvertOrderType;
+ bundles: IMap;
+}): IMap => {
+ const output = IMap().asMutable();
+
+ let unknownCounter = 1;
+ if (ConvertOrderType.PLACE_ORDERS == options.type) {
+ for (const bundle of options.bundles.get('orders').values()) {
+ let orderId = bundle.getIn(['events', 'wasm', 'order_idx']);
+
+ if (!orderId) {
+ orderId = `unknown_${unknownCounter++}`;
+ }
+
+ const denom = Denom.from(bundle.getIn(['events', 'wasm', 'offer_denom']));
+
+ const order = {
+ id: orderId,
+ clientId: bundle.getIn(['candidate']).clientId,
+ marketName: bundle.getIn(['market']).name,
+ marketId: bundle.getIn(['market']).id,
+ market: bundle.getIn(['market']),
+ ownerAddress:
+ bundle.getIn(['candidate']).type == OrderType.MARKET
+ ? bundle.getIn(['events', 'message', 'sender'])
+ : bundle.getIn(['candidate']).type == OrderType.LIMIT
+ ? bundle.getIn(['events', 'transfer', 'sender'])
+ : undefined,
+ payerAddress:
+ bundle.getIn(['candidate']).type == OrderType.MARKET
+ ? bundle.getIn(['events', 'message', 'sender'])
+ : bundle.getIn(['candidate']).type == OrderType.LIMIT
+ ? bundle.getIn(['events', 'transfer', 'sender'])
+ : undefined,
+ price: bundle.getIn(['events', 'wasm', 'quote_price'])
+ ? BigNumber(bundle.getIn(['events', 'wasm', 'quote_price']))
+ : BigNumber(bundle.getIn(['events', 'wasm-trade', 'quote_amount']))
+ .div(
+ BigNumber(bundle.getIn(['events', 'wasm-trade', 'base_amount']))
+ )
+ .decimalPlaces(bundle.getIn(['market', 'precision'])),
+ amount: bundle.getIn(['events', 'wasm', 'offer_amount'])
+ ? BigNumber(bundle.getIn(['events', 'wasm', 'offer_amount'])).div(
+ BigNumber(10).pow(denom.decimals)
+ )
+ : undefined,
+ side: convertOfferDenomToOrderSide(
+ bundle.getIn(['events', 'wasm', 'offer_denom']),
+ bundle.getIn(['market'])
+ ),
+ status: options.bundles.getIn(['common', 'status']),
+ type: bundle.getIn(['candidate']).type || OrderType.LIMIT,
+ fee: convertKujiraFeeToFee(
+ options.bundles.getIn(['common', 'events', 'tx', 'fee']) as string
+ ),
+ creationTimestamp: undefined,
+ fillingTimestamp: undefined,
+ hashes: {
+ creation: options.bundles.getIn([
+ 'common',
+ 'response',
+ 'transactionHash',
+ ]),
+ } as TransactionHashes,
+ connectorOrder: bundle.getIn(['common', 'response']),
+ } as Order;
+
+ output.set(orderId, order);
+ }
+ } else if (ConvertOrderType.GET_ORDERS == options.type) {
+ for (const bundle of options.bundles.get('orders')) {
+ let orderId = bundle['idx'];
+
+ if (!orderId) {
+ orderId = `unknown_${unknownCounter++}`;
+ }
+
+ const market = options.bundles.getIn(['common', 'market']) as Market;
+
+ const denom = Denom.from(bundle['offer_denom']['native']);
+
+ const order = {
+ id: orderId,
+ clientId: undefined,
+ marketName: market.name,
+ marketId: market.id,
+ market: market,
+ ownerAddress: bundle['owner'],
+ payerAddress: bundle['owner'],
+ price: bundle['quote_price']
+ ? BigNumber(bundle['quote_price'])
+ : undefined,
+ amount: bundle['original_offer_amount']
+ ? BigNumber(bundle['original_offer_amount']).div(
+ BigNumber(10).pow(denom.decimals)
+ )
+ : undefined,
+ side: convertOfferDenomToOrderSide(
+ bundle['offer_denom']['native'],
+ market
+ ),
+ status: convertKujiraOrderToStatus(bundle),
+ type: OrderType.LIMIT,
+ fee: undefined,
+ fillingTimestamp: undefined,
+ creationTimestamp: Number(bundle['created_at']),
+ hashes: undefined,
+ connectorOrder: bundle,
+ } as Order;
+
+ output.set(orderId, order);
+ }
+ } else if (ConvertOrderType.CANCELLED_ORDERS == options.type) {
+ for (const bundle of options.bundles.get('orders').values()) {
+ let orderId = bundle.getIn(['id']);
+
+ if (!orderId) {
+ orderId = `unknown_${unknownCounter++}`;
+ }
+
+ const order = {
+ id: orderId,
+ clientId: undefined,
+ marketName: bundle.getIn(['market']).name,
+ marketId: bundle.getIn(['market']).id,
+ market: bundle.getIn(['market']),
+ ownerAddress: options.bundles.getIn([
+ 'common',
+ 'events',
+ 'transfer',
+ 'sender',
+ ]),
+ payerAddress: options.bundles.getIn([
+ 'common',
+ 'events',
+ 'transfer',
+ 'sender',
+ ]),
+ price: undefined as unknown as OrderPrice,
+ amount: undefined as unknown as OrderAmount,
+ side: undefined as unknown as OrderSide,
+ status: OrderStatus.CANCELLED,
+ type: OrderType.LIMIT,
+ fee: convertKujiraFeeToFee(
+ options.bundles.getIn(['common', 'events', 'tx', 'fee']) as string
+ ),
+ creationTimestamp: undefined,
+ fillingTimestamp: undefined,
+ hashes: {
+ cancellation: options.bundles.getIn([
+ 'common',
+ 'response',
+ 'transactionHash',
+ ]),
+ } as TransactionHashes,
+ connectorOrder: bundle.getIn(['common', 'response']),
+ } as Order;
+
+ output.set(orderId, order);
+ }
+ }
+
+ return output;
+};
+
+export const convertKujiraTickerToTicker = (
+ input: KujiraTicker,
+ market: Market
+): Ticker => {
+ const price = BigNumber(input.price);
+ const timestamp = Date.now();
+
+ return {
+ market: market,
+ price: price,
+ timestamp: timestamp,
+ ticker: input,
+ };
+};
+
+export const convertKujiraBalancesToBalances = (
+ network: string,
+ balances: readonly Coin[],
+ orders: IMap,
+ tickers: IMap
+): Balances => {
+ const uskToken =
+ network.toLowerCase() == NETWORKS[MAINNET].toLowerCase()
+ ? convertKujiraTokenToToken(USK)
+ : convertKujiraTokenToToken(USK_TESTNET);
+
+ const output: Balances = {
+ tokens: IMap().asMutable(),
+ total: {
+ token: uskToken,
+ free: BigNumber(0),
+ lockedInOrders: BigNumber(0),
+ unsettled: BigNumber(0),
+ },
+ };
+
+ for (const balance of balances) {
+ const token = convertKujiraTokenToToken(Denom.from(balance.denom));
+ const ticker = tickers
+ .valueSeq()
+ .filter(
+ (ticker) =>
+ ticker.market.baseToken.id == token.id &&
+ ticker.market.quoteToken.id == uskToken.id
+ )
+ .first();
+ const amount = BigNumber(balance.amount).div(
+ BigNumber(10).pow(token.decimals)
+ );
+ const price = token.id == uskToken.id ? 1 : ticker?.price || 0;
+ output.tokens.set(token.id, {
+ token: token,
+ ticker: ticker,
+ free: amount,
+ lockedInOrders: BigNumber(0),
+ unsettled: BigNumber(0),
+ });
+
+ output.total.free = output.total.free.plus(amount.multipliedBy(price));
+ }
+
+ for (const order of orders.values()) {
+ const token =
+ order.side == OrderSide.BUY
+ ? order.market.quoteToken
+ : order.market.baseToken;
+
+ const ticker = tickers
+ .valueSeq()
+ .filter(
+ (ticker) =>
+ ticker.market.baseToken.id == token.id &&
+ ticker.market.quoteToken.id == uskToken.id
+ )
+ .first();
+
+ const amount = order.amount;
+ const price = token.id == uskToken.id ? 1 : ticker?.price || 0;
+
+ if (!output.tokens.has(token.id)) {
+ output.tokens.set(token.id, {
+ token: token,
+ ticker: ticker,
+ free: BigNumber(0),
+ lockedInOrders: BigNumber(0),
+ unsettled: BigNumber(0),
+ });
+ }
+
+ const tokenBalance = getNotNullOrThrowError(
+ output.tokens.get(token.id)
+ );
+
+ if (order.status == OrderStatus.OPEN) {
+ tokenBalance.lockedInOrders = tokenBalance.lockedInOrders.plus(amount);
+ output.total.lockedInOrders = output.total.lockedInOrders.plus(
+ amount.multipliedBy(price)
+ );
+ } else if (order.status == OrderStatus.FILLED) {
+ tokenBalance.unsettled = tokenBalance.unsettled.plus(amount);
+ output.total.unsettled = output.total.unsettled.plus(
+ amount.multipliedBy(price)
+ );
+ }
+ }
+
+ return output;
+};
+
+export const convertKujiraTransactionToTransaction = (
+ input: IndexedTx
+): Transaction => {
+ return {
+ hash: input.hash,
+ blockNumber: input.height,
+ gasUsed: input.gasUsed,
+ gasWanted: input.gasWanted,
+ code: input.code,
+ data: new TextDecoder('utf-8').decode(input.tx),
+ };
+};
+
+export const convertKujiraSettlementToSettlement = (
+ input: KujiraWithdraw
+): Withdraw => {
+ return {
+ hash: input.transactionHash,
+ };
+};
+
+export const convertNetworkToKujiraNetwork = (
+ input: string
+): keyof typeof contracts => {
+ input = input.toLowerCase();
+ let output: keyof typeof contracts;
+
+ if (input.toLowerCase() == 'mainnet') {
+ output = MAINNET;
+ } else if (input.toLowerCase() == 'testnet') {
+ output = TESTNET;
+ } else {
+ throw new Error(`Unrecognized network: ${input}`);
+ }
+
+ return output;
+};
+
+export const convertKujiraEventsToMapOfEvents = (
+ events: readonly KujiraEvent[]
+): IMap => {
+ const output = IMap().asMutable();
+
+ for (const event of events) {
+ for (const attribute of event.attributes) {
+ if (!output.getIn([event.type, attribute.key])) {
+ output.setIn([event.type, attribute.key], attribute.value);
+ }
+ }
+ }
+
+ return output;
+};
+
+export const convertKujiraRawLogEventsToMapOfEvents = (
+ eventsLog: Array,
+ cancelManyOrderNumber?: number
+): IMap => {
+ if (cancelManyOrderNumber) {
+ let msgIndex = (eventsLog[0]['msg_index'] as number) + 1;
+ for (let i = 0; i < cancelManyOrderNumber - 1; i++) {
+ const newEventLog = { ...eventsLog[0] };
+ newEventLog['msg_index'] = msgIndex;
+ eventsLog.push(newEventLog);
+ msgIndex = msgIndex + 1;
+ }
+ }
+ const output = IMap().asMutable();
+ for (const eventLog of eventsLog) {
+ const bundleIndex = eventLog['msg_index'];
+ const events = eventLog['events'];
+ for (const event of events) {
+ for (const attribute of event.attributes) {
+ output.setIn([bundleIndex, event.type, attribute.key], attribute.value);
+ }
+ }
+ }
+
+ return output;
+};
+
+export const convertToResponseBody = (input: any): any => {
+ let output = input;
+
+ if (IMap.isMap(input)) output = input.toJS();
+ for (const key in output) {
+ if (IMap.isMap(output[key])) {
+ output[key] = output[key].toJS();
+ }
+ }
+
+ return output;
+};
+
+export function convertNonStandardKujiraTokenIds(
+ tokensIds: TokenId[]
+): TokenId[] {
+ const output: TokenId[] = [];
+
+ for (const tokenId of tokensIds) {
+ if (tokenId.startsWith('ibc')) {
+ const denom = Denom.from(tokenId);
+
+ if (denom.trace && denom.trace.base_denom) {
+ output.push(
+ getNotNullOrThrowError(denom.trace?.base_denom).replace(
+ ':',
+ '/'
+ )
+ );
+ }
+ }
+ }
+
+ return output;
+}
+
+export function convertClobBatchOrdersRequestToKujiraPlaceOrdersRequest(
+ obj: any
+): any {
+ if (Array.isArray(obj)) {
+ return obj.map((item) =>
+ convertClobBatchOrdersRequestToKujiraPlaceOrdersRequest(item)
+ );
+ } else if (typeof obj === 'object' && obj !== null) {
+ const updatedObj: any = {};
+ for (const key in obj) {
+ let newKey = key;
+ let value = obj[key];
+ if (key === 'orderType') {
+ newKey = 'type';
+ } else if (key === 'market') {
+ value = value.replace('-', '/');
+ newKey = 'marketId';
+ }
+ updatedObj[newKey] =
+ convertClobBatchOrdersRequestToKujiraPlaceOrdersRequest(value);
+ }
+ return updatedObj;
+ } else {
+ return obj;
+ }
+}
+
+export function convertClobBatchOrdersRequestToKujiraCancelOrdersRequest(
+ obj: any
+): any {
+ const { cancelOrderParams, address, ...rest } = obj;
+ const ids = [];
+ const idsFromCancelOrderParams: ClobDeleteOrderRequestExtract[] =
+ cancelOrderParams;
+ for (const key of idsFromCancelOrderParams) {
+ ids.push(key.orderId);
+ }
+ const marketId = cancelOrderParams[0].market;
+
+ return {
+ ...rest,
+ ids: ids,
+ marketId: marketId,
+ ownerAddress: address,
+ };
+}
diff --git a/src/connectors/kujira/kujira.helpers.ts b/src/connectors/kujira/kujira.helpers.ts
new file mode 100644
index 0000000000..a5eb2f7172
--- /dev/null
+++ b/src/connectors/kujira/kujira.helpers.ts
@@ -0,0 +1,148 @@
+import { KujiraConfig } from './kujira.config';
+
+/**
+ *
+ * @param value
+ * @param errorMessage
+ */
+export const getNotNullOrThrowError = (
+ value?: any,
+ errorMessage: string = 'Value is null or undefined'
+): R => {
+ if (value === undefined || value === null) throw new Error(errorMessage);
+
+ return value as R;
+};
+
+/**
+ *
+ * @param milliseconds
+ */
+export const sleep = (milliseconds: number) =>
+ new Promise((callback) => setTimeout(callback, milliseconds));
+
+/**
+ * Same as Promise.all(items.map(item => task(item))), but it waits for
+ * the first {batchSize} promises to finish before starting the next batch.
+ *
+ * @template A
+ * @template B
+ * @param {function(A): B} task The task to run for each item.
+ * @param {A[]} items Arguments to pass to the task for each call.
+ * @param {int} batchSize The number of items to process at a time.
+ * @param {int} delayBetweenBatches Delay between each batch (milliseconds).
+ * @returns {B[]}
+ */
+export const promiseAllInBatches = async (
+ task: (item: I) => Promise,
+ items: any[],
+ batchSize: number = KujiraConfig.config.parallel.all.batchSize,
+ delayBetweenBatches: number = KujiraConfig.config.parallel.all
+ .delayBetweenBatches
+): Promise => {
+ let position = 0;
+ let results: any[] = [];
+
+ if (!batchSize) {
+ batchSize = items.length;
+ }
+
+ while (position < items.length) {
+ const itemsForBatch = items.slice(position, position + batchSize);
+ results = [
+ ...results,
+ ...(await Promise.all(itemsForBatch.map((item) => task(item)))),
+ ];
+ position += batchSize;
+
+ if (position < items.length) {
+ if (delayBetweenBatches > 0) {
+ await sleep(delayBetweenBatches);
+ }
+ }
+ }
+
+ return results;
+};
+
+/**
+ * @param targetObject
+ * @param targetFunction
+ * @param targetParameters
+ * @param maxNumberOfRetries 0 means no retries
+ * @param delayBetweenRetries 0 means no delay (milliseconds)
+ * @param timeout 0 means no timeout (milliseconds)
+ * @param timeoutMessage
+ */
+export const runWithRetryAndTimeout = async (
+ targetObject: any,
+ targetFunction: (...args: any[]) => R,
+ targetParameters: any,
+ maxNumberOfRetries: number = KujiraConfig.config.retry.all.maxNumberOfRetries,
+ delayBetweenRetries: number = KujiraConfig.config.retry.all
+ .delayBetweenRetries,
+ timeout: number = KujiraConfig.config.timeout.all,
+ timeoutMessage: string = 'Timeout exceeded.'
+): Promise => {
+ const errors = [];
+ let retryCount = 0;
+ let timer: any;
+
+ if (timeout > 0) {
+ timer = setTimeout(() => new Error(timeoutMessage), timeout);
+ }
+
+ do {
+ try {
+ const result = await targetFunction.apply(targetObject, targetParameters);
+
+ if (timeout > 0) {
+ clearTimeout(timer);
+ }
+
+ return result as R;
+ } catch (error: any) {
+ errors.push(error);
+
+ retryCount++;
+
+ console.debug(
+ `${targetObject?.constructor.name || targetObject}:${
+ targetFunction.name
+ } => retry ${retryCount} of ${maxNumberOfRetries}`
+ );
+
+ if (retryCount < maxNumberOfRetries) {
+ if (delayBetweenRetries > 0) {
+ await sleep(delayBetweenRetries);
+ }
+ } else {
+ const allErrors = Error(
+ `Failed to execute "${
+ targetFunction.name
+ }" with ${maxNumberOfRetries} retries. All error messages were:\n${errors
+ .map((error: any) => error.message)
+ .join(';\n')}\n`
+ );
+
+ allErrors.stack = error.stack;
+
+ console.error(allErrors);
+
+ throw allErrors;
+ }
+ }
+ } while (retryCount < maxNumberOfRetries);
+
+ throw Error('Unknown error.');
+};
+
+export const isValidKujiraPublicKey = (publicKey: string): boolean => {
+ return /^kujira([a-z0-9]){39}$/.test(publicKey);
+};
+
+export const isKujiraPrivateKey = (privateKey: string): boolean => {
+ return /^(?:\b[a-z]+\b(?:\s|$)){12}(?:(?:\b[a-z]+\b(?:\s|$)){12})?$/.test(
+ privateKey
+ );
+};
diff --git a/src/connectors/kujira/kujira.model.ts b/src/connectors/kujira/kujira.model.ts
new file mode 100644
index 0000000000..ef09fcf570
--- /dev/null
+++ b/src/connectors/kujira/kujira.model.ts
@@ -0,0 +1,2001 @@
+import {
+ Address,
+ AllMarketsWithdrawsRequest,
+ AllMarketsWithdrawsResponse,
+ Balance,
+ Balances,
+ BasicKujiraMarket,
+ BasicKujiraToken,
+ CancelAllOrdersRequest,
+ CancelAllOrdersResponse,
+ CancelOrderRequest,
+ CancelOrderResponse,
+ CancelOrdersRequest,
+ CancelOrdersResponse,
+ ConvertOrderType,
+ DecryptWalletRequest,
+ DecryptWalletResponse,
+ EncryptWalletRequest,
+ EncryptWalletResponse,
+ EstimatedFees,
+ GetAllBalancesRequest,
+ GetAllBalancesResponse,
+ GetAllMarketsRequest,
+ GetAllMarketsResponse,
+ GetAllOrderBooksRequest,
+ GetAllOrderBooksResponse,
+ GetAllTickersRequest,
+ GetAllTickersResponse,
+ GetAllTokensRequest,
+ GetAllTokensResponse,
+ GetBalanceRequest,
+ GetBalanceResponse,
+ GetBalancesRequest,
+ GetBalancesResponse,
+ GetCurrentBlockRequest,
+ GetCurrentBlockResponse,
+ GetEstimatedFeesRequest,
+ GetEstimatedFeesResponse,
+ GetMarketRequest,
+ GetMarketResponse,
+ GetMarketsRequest,
+ GetMarketsResponse,
+ GetOrderBookRequest,
+ GetOrderBookResponse,
+ GetOrderBooksRequest,
+ GetOrderBooksResponse,
+ GetOrderRequest,
+ GetOrderResponse,
+ GetOrdersRequest,
+ GetOrdersResponse,
+ GetRootRequest,
+ GetRootResponse,
+ GetTickerRequest,
+ GetTickerResponse,
+ GetTickersRequest,
+ GetTickersResponse,
+ GetTokenRequest,
+ GetTokenResponse,
+ GetTokensRequest,
+ GetTokensResponse,
+ GetTokenSymbolsToTokenIdsMapRequest,
+ GetTokenSymbolsToTokenIdsMapResponse,
+ GetTransactionRequest,
+ GetTransactionResponse,
+ GetTransactionsRequest,
+ GetTransactionsResponse,
+ GetWalletArtifactsRequest,
+ GetWalletPublicKeyRequest,
+ GetWalletPublicKeyResponse,
+ GetWalletsPublicKeysRequest,
+ IMap,
+ KujiraOrder,
+ KujiraWalletArtifacts,
+ LatencyData,
+ Market,
+ MarketId,
+ MarketName,
+ MarketNotFoundError,
+ MarketsWithdrawsFundsResponse,
+ MarketsWithdrawsRequest,
+ MarketWithdrawRequest,
+ MarketWithdrawResponse,
+ Mnemonic,
+ Order,
+ OrderBook,
+ OrderId,
+ OrderNotFoundError,
+ OrderOwnerAddress,
+ OrderPrice,
+ OrderSide,
+ OrderStatus,
+ OrderType,
+ OwnerAddress,
+ PlaceOrderRequest,
+ PlaceOrderResponse,
+ PlaceOrdersRequest,
+ PlaceOrdersResponse,
+ Ticker,
+ Token,
+ TokenId,
+ TokenNotFoundError,
+ TokenSymbol,
+ Transaction,
+ TransactionHash,
+ TransferFromToRequest,
+ TransferFromToResponse,
+ Withdraw,
+} from './kujira.types';
+import { KujiraConfig, NetworkConfig } from './kujira.config';
+import { Slip10RawIndex } from '@cosmjs/crypto';
+import {
+ getNotNullOrThrowError,
+ promiseAllInBatches,
+ runWithRetryAndTimeout,
+} from './kujira.helpers';
+import {
+ Denom,
+ fin,
+ KujiraQueryClient,
+ kujiraQueryClient,
+ MAINNET,
+ msg,
+ NETWORKS,
+ registry,
+ RPCS,
+ USK,
+ USK_TESTNET,
+} from 'kujira.js';
+import contracts from 'kujira.js/src/resources/contracts.json';
+import {
+ convertKujiraBalancesToBalances,
+ convertKujiraEventsToMapOfEvents,
+ convertKujiraMarketToMarket,
+ convertKujiraOrderBookToOrderBook,
+ convertKujiraOrdersToMapOfOrders,
+ convertKujiraRawLogEventsToMapOfEvents,
+ convertKujiraSettlementToSettlement,
+ convertKujiraTickerToTicker,
+ convertKujiraTokenToToken,
+ convertKujiraTransactionToTransaction,
+ convertNetworkToKujiraNetwork,
+ convertNonStandardKujiraTokenIds,
+} from './kujira.convertors';
+import {
+ coins,
+ GasPrice,
+ SigningStargateClient,
+ StargateClient,
+} from '@cosmjs/stargate';
+import { ExecuteResult, JsonObject } from '@cosmjs/cosmwasm-stargate';
+import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate/build/signingcosmwasmclient';
+import {
+ Coin,
+ DirectSecp256k1HdWallet,
+ EncodeObject,
+} from '@cosmjs/proto-signing';
+import { HttpBatchClient, Tendermint34Client } from '@cosmjs/tendermint-rpc';
+import { StdFee } from '@cosmjs/amino';
+import { IndexedTx } from '@cosmjs/stargate/build/stargateclient';
+import { BigNumber } from 'bignumber.js';
+import { walletPath } from '../../services/base';
+import fse from 'fs-extra';
+import { ConfigManagerCertPassphrase } from '../../services/config-manager-cert-passphrase';
+import * as crypto from 'crypto';
+import util from 'util';
+import { promises as fs } from 'fs';
+
+const pbkdf2 = util.promisify(crypto.pbkdf2);
+
+const config = KujiraConfig.config;
+
+/**
+ *
+ */
+export class KujiraModel {
+ /**
+ *
+ * @private
+ */
+ private isInitializing: boolean = false;
+
+ /**
+ *
+ * @private
+ */
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ private rpcEndpoint: string;
+
+ /**
+ * The correct type for this property would be kujira.js/NETWORK
+ * but the compile method is incompatible with it.
+ *
+ * @private
+ */
+ private readonly kujiraNetwork: keyof typeof contracts;
+
+ /**
+ *
+ * @private
+ */
+ private accounts: IMap;
+
+ /**
+ *
+ * @private
+ */
+ private basicMarkets: IMap = IMap<
+ MarketId,
+ BasicKujiraMarket
+ >();
+
+ /**
+ *
+ * @private
+ */
+ private basicTokens: IMap = IMap<
+ TokenId,
+ BasicKujiraToken
+ >();
+
+ /**
+ *
+ * @private
+ */
+ private markets: IMap = IMap();
+
+ /**
+ *
+ * @private
+ */
+ private tokens: IMap = IMap();
+
+ /**
+ *
+ * @private
+ */
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ private httpBatchClient: HttpBatchClient;
+
+ /**
+ *
+ * @private
+ */
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ private tendermint34Client: Tendermint34Client;
+
+ /**
+ *
+ * @private
+ */
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ private kujiraQueryClient: KujiraQueryClient;
+
+ /**
+ *
+ * @private
+ */
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ private stargateClient: StargateClient;
+
+ /**
+ *
+ */
+ chain: string;
+
+ /**
+ *
+ */
+ network: string;
+
+ /**
+ *
+ */
+ readonly connector: string = 'kujira';
+
+ /**
+ *
+ */
+ isReady: boolean = false;
+
+ /**
+ *
+ * @private
+ */
+ private static _instances: { [name: string]: KujiraModel };
+
+ /**
+ * Get the Kujira instance for the given chain and network.
+ *
+ * @param chain
+ * @param network
+ */
+ public static getInstance(chain: string, network: string): KujiraModel {
+ if (KujiraModel._instances === undefined) {
+ KujiraModel._instances = {};
+ }
+
+ const key = `${chain}:${network}`;
+
+ if (!(key in KujiraModel._instances)) {
+ KujiraModel._instances[key] = new KujiraModel(chain, network);
+ }
+
+ return KujiraModel._instances[key];
+ }
+
+ /**
+ *
+ */
+ public static getConnectedInstances(): { [key: string]: KujiraModel } {
+ return KujiraModel._instances;
+ }
+
+ /**
+ * Creates a new instance of Kujira.
+ *
+ * @param chain
+ * @param network
+ * @private
+ */
+ private constructor(chain: string, network: string) {
+ this.chain = chain;
+ this.network = network;
+
+ this.kujiraNetwork = convertNetworkToKujiraNetwork(this.network);
+
+ this.accounts = IMap().asMutable();
+ }
+
+ private async getRPCEndpoint(): Promise {
+ if (!this.rpcEndpoint) {
+ this.rpcEndpoint =
+ getNotNullOrThrowError(
+ getNotNullOrThrowError