diff --git a/.husky/pre-commit b/.husky/pre-commit index 5369480d..e0973e00 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,3 +2,4 @@ . "$(dirname "$0")/_/husky.sh" yarn lint-staged +yarn generate-barrels diff --git a/jest.config.ts b/jest.config.ts index 1b0973e7..e7dd6fdd 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -22,6 +22,8 @@ const config: Config = { "!jest.config.ts", "!**/src/gql/utils/generated.ts", "!**/src/sdk/utils/testutil.ts", + "!**/src/sdk/core/cosmwasmclient.ts", // Implementation from Cosmjs + "!**/src/sdk/core/signingcosmwasmclient.ts", // Implementation from Cosmjs ], testPathIgnorePatterns: ["/node_modules/", "/dist/", "/nibiru/"], coverageReporters: ["json-summary", "text", "html", "lcov"], diff --git a/package.json b/package.json index 4488fd48..fa2ba827 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "bignumber.js": "^9.1.1", "cross-fetch": "4.0.0", "graphql": "^16.7.1", - "graphql-ws": "^5.14.0" + "graphql-ws": "^5.14.0", + "pako": "^2.1.0" }, "peerDependencies": { "@cosmjs/cosmwasm-stargate": "^0.32.3", @@ -67,6 +68,7 @@ "@types/jest": "^29.1.2", "@types/long": "^4.0.0", "@types/node": "^16.11.7", + "@types/pako": "^2.0.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.30.7", "barrelsby": "^2.8.1", diff --git a/src/gql/heart-monitor/heart-monitor.test.ts b/src/gql/heart-monitor/heart-monitor.test.ts index 374210a9..b2c87c4e 100644 --- a/src/gql/heart-monitor/heart-monitor.test.ts +++ b/src/gql/heart-monitor/heart-monitor.test.ts @@ -7,8 +7,6 @@ import { QueryGovernanceArgs, QueryIbcArgs, QueryOracleArgs, - QueryStatsArgs, - GQLStatsFields, communityPoolQueryString, QueryWasmArgs, GqlWasmFields, @@ -24,19 +22,12 @@ import { defaultIbcTransfer, defaultOracleEntry, defaultOraclePrice, - defaultPerpOpenInterest, - defaultPerpPnl, defaultRedelegations, - defaultStatsFees, defaultToken, - defaultTotals, - defaultTvl, defaultUnbondings, defaultUser, defaultUserContract, - defaultUsers, defaultValidator, - defaultVolume, GQLDistributionCommission, GQLOraclePrice, GQLQueryGqlCommunityPoolArgs, @@ -414,28 +405,6 @@ test("queryBatchHandler", async () => { } }) -const testStats = async (args: QueryStatsArgs, fields: GQLStatsFields) => { - const resp = await heartMonitor.stats(args, fields) - expect(resp).toHaveProperty("stats") - - if (resp.GQLStats) { - const { GQLStats } = resp - - checkFields( - [GQLStats], - [ - "totals", - "fees", - "perpOpenInterest", - "tvl", - "perpPnl", - "users", - "volume", - ] - ) - } -} - const testStaking = async ( args: QueryStakingArgs, fields: GQLStakingFields @@ -501,55 +470,6 @@ test.skip("staking", async () => { ) }) -test("stats", async () => { - await testStats( - { - totals: { - limit: 1, - }, - fees: { - limit: 1, - }, - perpOpenInterest: { - limit: 1, - }, - tvl: { - limit: 1, - }, - perpPnl: { - limit: 1, - }, - users: { - limit: 1, - }, - volume: { - limit: 1, - }, - }, - { - totals: defaultTotals, - fees: defaultStatsFees, - perpOpenInterest: defaultPerpOpenInterest, - tvl: defaultTvl, - perpPnl: defaultPerpPnl, - users: defaultUsers, - volume: defaultVolume, - } - ) - await testStats( - {}, - { - totals: defaultTotals, - fees: defaultStatsFees, - perpOpenInterest: defaultPerpOpenInterest, - tvl: defaultTvl, - perpPnl: defaultPerpPnl, - users: defaultUsers, - volume: defaultVolume, - } - ) -}) - const testWasm = async (args: QueryWasmArgs, fields: GqlWasmFields) => { const resp = await heartMonitor.wasm(args, fields) expect(resp).toHaveProperty("wasm") diff --git a/src/gql/heart-monitor/heart-monitor.ts b/src/gql/heart-monitor/heart-monitor.ts index 1ff4f11e..eeacbb15 100644 --- a/src/gql/heart-monitor/heart-monitor.ts +++ b/src/gql/heart-monitor/heart-monitor.ts @@ -16,10 +16,6 @@ import { communityPool, distributionCommissions, users, - GqlOutStats, - QueryStatsArgs, - GQLStatsFields, - stats, GqlOutGovernance, QueryGovernanceArgs, governance, @@ -118,11 +114,6 @@ export interface IHeartMonitor { fields: DeepPartial ) => Promise - readonly stats: ( - args: QueryStatsArgs, - fields: DeepPartial - ) => Promise - readonly user: ( args: GQLQueryGqlUserArgs, fields: DeepPartial @@ -213,9 +204,6 @@ export class HeartMonitor implements IHeartMonitor { fields: DeepPartial ) => staking(args, this.gqlEndpt, fields) - stats = async (args: QueryStatsArgs, fields: DeepPartial) => - stats(args, this.gqlEndpt, fields) - user = async (args: GQLQueryGqlUserArgs, fields: DeepPartial) => user(args, this.gqlEndpt, fields) diff --git a/src/gql/query/index.ts b/src/gql/query/index.ts index d82f41ca..24fc9918 100644 --- a/src/gql/query/index.ts +++ b/src/gql/query/index.ts @@ -11,7 +11,6 @@ export * from "./inflation" export * from "./oracle" export * from "./proxies" export * from "./staking" -export * from "./stats" export * from "./user" export * from "./users" export * from "./wasm" diff --git a/src/gql/query/stats.ts b/src/gql/query/stats.ts deleted file mode 100644 index c388e8a9..00000000 --- a/src/gql/query/stats.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { - convertObjectToPropertiesString, - doGqlQuery, - gqlQuery, - GQLQuery, - GQLStatsGqlFeesArgs, - GQLStatsFees, - GQLStatsPerpOpenInterest, - GQLStatsPerpPnl, - GQLStatsTotals, - GQLStatsTvl, - GQLStatsUsers, - GQLStatsVolume, - GQLStatsGqlPerpOpenInterestArgs, - GQLStatsGqlPerpPnlArgs, - GQLStatsGqlTotalsArgs, - GQLStatsGqlTvlArgs, - GQLStatsGqlUsersArgs, - GQLStatsGqlVolumeArgs, - DeepPartial, -} from ".." - -export type QueryStatsArgs = { - fees?: GQLStatsGqlFeesArgs - perpOpenInterest?: GQLStatsGqlPerpOpenInterestArgs - perpPnl?: GQLStatsGqlPerpPnlArgs - totals?: GQLStatsGqlTotalsArgs - tvl?: GQLStatsGqlTvlArgs - users?: GQLStatsGqlUsersArgs - volume?: GQLStatsGqlVolumeArgs -} - -export interface GqlOutStats { - GQLStats?: GQLQuery["stats"] -} - -export type GQLStatsFields = DeepPartial<{ - fees?: DeepPartial - perpOpenInterest?: DeepPartial - perpPnl?: DeepPartial - totals?: DeepPartial - tvl?: DeepPartial - users?: DeepPartial - volume?: DeepPartial -}> - -export const GQLStatsQueryString = ( - args: QueryStatsArgs, - fields: GQLStatsFields -) => { - const GQLStatsQuery: string[] = [] - - if (fields.fees) { - GQLStatsQuery.push( - gqlQuery( - "fees", - args.fees ?? {}, - convertObjectToPropertiesString(fields.fees), - true - ) - ) - } - - if (fields.perpOpenInterest) { - GQLStatsQuery.push( - gqlQuery( - "perpOpenInterest", - args.perpOpenInterest ?? {}, - convertObjectToPropertiesString(fields.perpOpenInterest), - true - ) - ) - } - - if (fields.perpPnl) { - GQLStatsQuery.push( - gqlQuery( - "perpPnl", - args.perpPnl ?? {}, - convertObjectToPropertiesString(fields.perpPnl), - true - ) - ) - } - - if (fields.totals) { - GQLStatsQuery.push( - gqlQuery( - "totals", - args.totals ?? {}, - convertObjectToPropertiesString(fields.totals), - true - ) - ) - } - - if (fields.tvl) { - GQLStatsQuery.push( - gqlQuery( - "tvl", - args.tvl ?? {}, - convertObjectToPropertiesString(fields.tvl), - true - ) - ) - } - - if (fields.users) { - GQLStatsQuery.push( - gqlQuery( - "users", - args.users ?? {}, - convertObjectToPropertiesString(fields.users), - true - ) - ) - } - - if (fields.volume) { - GQLStatsQuery.push( - gqlQuery( - "volume", - args.volume ?? {}, - convertObjectToPropertiesString(fields.volume), - true - ) - ) - } - - return ` - stats { - ${GQLStatsQuery.join("\n")} - } - ` -} - -export const stats = async ( - args: QueryStatsArgs, - endpt: string, - fields: GQLStatsFields -): Promise => - doGqlQuery( - `{ - ${GQLStatsQueryString(args, fields)} - }`, - endpt - ) diff --git a/src/gql/utils/defaultObjects.ts b/src/gql/utils/defaultObjects.ts index 72cf978e..09ec535d 100644 --- a/src/gql/utils/defaultObjects.ts +++ b/src/gql/utils/defaultObjects.ts @@ -28,13 +28,6 @@ import { GQLSpotPoolSwap, GQLStakingActionType, GQLStakingHistoryItem, - GQLStatsFees, - GQLStatsPerpOpenInterest, - GQLStatsPerpPnl, - GQLStatsTotals, - GQLStatsTvl, - GQLStatsUsers, - GQLStatsVolume, GQLToken, GQLUnbonding, GQLUser, @@ -268,101 +261,6 @@ export const defaultSpotPoolSwap: GQLSpotPoolSwap = { user: defaultUser, } -export const defaultStatsFees: GQLStatsFees = { - feesLiquidations: 0, - feesLiquidationsCumulative: 0, - feesPerp: 0, - feesPerpCumulative: 0, - feesSwap: 0, - feesSwapCumulative: 0, - feesTotal: 0, - feesTotalCumulative: 0, - period: 0, - periodInterval: "", - periodStartTs: "", -} - -export const defaultPerpOpenInterest: GQLStatsPerpOpenInterest = { - openInterestLong: 0, - openInterestShort: 0, - openInterestTotal: 0, - period: 0, - periodInterval: "", - periodStartTs: "", -} - -export const defaultPerpPnl: GQLStatsPerpPnl = { - loss: 0, - lossCumulative: 0, - netPnl: 0, - netPnlCumulative: 0, - period: 0, - periodInterval: "", - periodStartTs: "", - profit: 0, - profitCumulative: 0, -} - -export const defaultTotals: GQLStatsTotals = { - period: 0, - periodInterval: "", - periodStartTs: "", - totalPerp: 0, - totalFeesPerp: 0, - totalFeesLiquidations: 0, - totalOpenInterest: 0, - totalTransactions: 0, - totalSwap: 0, - totalTvl: 0, -} - -export const defaultTvl: GQLStatsTvl = { - period: 0, - periodInterval: "", - periodStartTs: "", - tvlPerp: 0, - tvlStablecoin: 0, - tvlStaking: 0, - tvlSwap: 0, - tvlTotal: 0, -} - -export const defaultUsers: GQLStatsUsers = { - newUsersLp: 0, - newUsersLpCumulative: 0, - newUsersPerp: 0, - newUsersPerpCumulative: 0, - newUsersSwap: 0, - newUsersSwapCumulative: 0, - newUsersTotal: 0, - newUsersTotalCumulative: 0, - period: 0, - periodInterval: "", - periodStartTs: "", - userActionsPerp: 0, - uniqueUsersLp: 0, - uniqueUsersPerp: 0, - uniqueUsersSwap: 0, - uniqueUsersTotal: 0, - userActionsLp: 0, - userActionsSwap: 0, - userActionsTotal: 0, - newAuthUsers: 0, - newAuthUsersCumulative: 0, -} - -export const defaultVolume: GQLStatsVolume = { - volumePerp: 0, - volumePerpCumulative: 0, - volumeSwap: 0, - volumeSwapCumulative: 0, - volumeTotal: 0, - volumeTotalCumulative: 0, - period: 0, - periodInterval: "", - periodStartTs: "", -} - export const defaultUnbondings: GQLUnbonding = { amount: 0, completion_time: "", @@ -444,8 +342,8 @@ export const defaultInflationInfo: GQLInflationInfo = { export const defaultFeatureFlags: GQLFeatureFlags = { gov: true, oracle: true, - perp: true, - spot: true, + perp: false, + spot: false, staking: true, wasm: true, } diff --git a/src/sdk/core/cosmwasmclient.ts b/src/sdk/core/cosmwasmclient.ts new file mode 100644 index 00000000..33fbcafa --- /dev/null +++ b/src/sdk/core/cosmwasmclient.ts @@ -0,0 +1,62 @@ +import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate" +import { + Account, + accountFromAny, + AccountParser, + HttpEndpoint, + SequenceResponse, +} from "@cosmjs/stargate" +import { CometClient } from "@cosmjs/tendermint-rpc" + +export interface NibiCosmWasmClientOptions { + readonly accountParser?: AccountParser +} + +export class NibiCosmWasmClient extends CosmWasmClient { + private readonly accountParser: AccountParser + + protected constructor( + cometClient: CometClient | undefined, + options: NibiCosmWasmClientOptions = {} + ) { + super(cometClient) + const { accountParser = accountFromAny } = options + this.accountParser = accountParser + } + + public static async connect( + endpoint: string | HttpEndpoint, + options: NibiCosmWasmClientOptions = {} + ): Promise { + const cosmWasmClient = await CosmWasmClient.connect(endpoint) + return new NibiCosmWasmClient(cosmWasmClient["cometClient"], options) + } + + public async getAccount(searchAddress: string): Promise { + try { + const account = await this.forceGetQueryClient().auth.account( + searchAddress + ) + return account ? this.accountParser(account) : null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if (/rpc error: code = NotFound/i.test(error.toString())) { + return null + } + throw error + } + } + + public async getSequence(address: string): Promise { + const account = await this.getAccount(address) + if (!account) { + throw new Error( + `Account '${address}' does not exist on chain. Send some tokens there before trying to query sequence.` + ) + } + return { + accountNumber: account.accountNumber, + sequence: account.sequence, + } + } +} diff --git a/src/sdk/core/signingcosmwasmclient.ts b/src/sdk/core/signingcosmwasmclient.ts new file mode 100644 index 00000000..8fd5df95 --- /dev/null +++ b/src/sdk/core/signingcosmwasmclient.ts @@ -0,0 +1,805 @@ +import { + encodeSecp256k1Pubkey, + makeSignDoc as makeSignDocAmino, +} from "@cosmjs/amino" +import { sha256 } from "@cosmjs/crypto" +import { fromBase64, toHex, toUtf8 } from "@cosmjs/encoding" +import { Int53, Uint53 } from "@cosmjs/math" +import { + ChangeAdminResult, + createWasmAminoConverters, + ExecuteInstruction, + ExecuteResult, + HttpEndpoint, + InstantiateOptions, + InstantiateResult, + JsonObject, + MigrateResult, + MsgClearAdminEncodeObject, + MsgExecuteContractEncodeObject, + MsgInstantiateContract2EncodeObject, + MsgInstantiateContractEncodeObject, + MsgMigrateContractEncodeObject, + MsgStoreCodeEncodeObject, + MsgUpdateAdminEncodeObject, + UploadResult, + wasmTypes, +} from "@cosmjs/cosmwasm-stargate" +import { assert, assertDefined } from "@cosmjs/utils" +import { + Coin, + EncodeObject, + encodePubkey, + isOfflineDirectSigner, + makeAuthInfoBytes, + makeSignDoc, + OfflineSigner, + Registry, + TxBodyEncodeObject, +} from "@cosmjs/proto-signing" +import { + AminoTypes, + DeliverTxResponse, + GasPrice, + defaultRegistryTypes as defaultStargateTypes, + MsgDelegateEncodeObject, + MsgSendEncodeObject, + MsgUndelegateEncodeObject, + MsgWithdrawDelegatorRewardEncodeObject, + SignerData, + StdFee, + calculateFee, + createDefaultAminoConverters, + isDeliverTxFailure, + logs, +} from "@cosmjs/stargate" +import { findAttribute } from "@cosmjs/cosmwasm-stargate/build/signingcosmwasmclient" +import { connectComet, CometClient } from "@cosmjs/tendermint-rpc" +import { NibiCosmWasmClient, NibiCosmWasmClientOptions } from "./cosmwasmclient" +import { AccessConfig } from "cosmjs-types/cosmwasm/wasm/v1/types" +import { + MsgClearAdmin, + MsgExecuteContract, + MsgInstantiateContract, + MsgInstantiateContract2, + MsgMigrateContract, + MsgStoreCode, + MsgUpdateAdmin, +} from "cosmjs-types/cosmwasm/wasm/v1/tx" +import pako from "pako" +import { + MsgDelegate, + MsgUndelegate, +} from "cosmjs-types/cosmos/staking/v1beta1/tx" +import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx" +import { SignMode } from "cosmjs-types/cosmos/tx/signing/v1beta1/signing" +import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx" + +function createDeliverTxResponseErrorMessage( + result: DeliverTxResponse +): string { + return `Error when broadcasting tx ${result.transactionHash} at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}` +} + +export interface NibiSigningCosmWasmClientOptions + extends NibiCosmWasmClientOptions { + readonly registry?: Registry + readonly aminoTypes?: AminoTypes + readonly broadcastTimeoutMs?: number + readonly broadcastPollIntervalMs?: number + readonly gasPrice?: GasPrice +} + +type FeeOption = StdFee | "auto" | number + +export class NibiSigningCosmWasmClient extends NibiCosmWasmClient { + public readonly registry: Registry + public readonly broadcastTimeoutMs: number | undefined + public readonly broadcastPollIntervalMs: number | undefined + + private readonly signer: OfflineSigner + private readonly aminoTypes: AminoTypes + private readonly gasPrice: GasPrice | undefined + // Starting with Cosmos SDK 0.47, we see many cases in which 1.3 is not enough anymore + // E.g. https://github.com/cosmos/cosmos-sdk/issues/16020 + private readonly defaultGasMultiplier = 1.4 + + /** + * Creates an instance by connecting to the given CometBFT RPC endpoint. + * + * This uses auto-detection to decide between a CometBFT 0.38, Tendermint 0.37 and 0.34 client. + * To set the Comet client explicitly, use `createWithSigner`. + */ + public static async connectWithSigner( + endpoint: string | HttpEndpoint, + signer: OfflineSigner, + options: NibiSigningCosmWasmClientOptions = {} + ): Promise { + const cometClient = await connectComet(endpoint) + return NibiSigningCosmWasmClient.createWithSigner( + cometClient, + signer, + options + ) + } + + /** + * Creates an instance from a manually created Comet client. + * Use this to use `Comet38Client` or `Tendermint37Client` instead of `Tendermint34Client`. + */ + public static async createWithSigner( + cometClient: CometClient, + signer: OfflineSigner, + options: NibiSigningCosmWasmClientOptions = {} + ): Promise { + return new NibiSigningCosmWasmClient(cometClient, signer, options) + } + + /** + * Creates a client in offline mode. + * + * This should only be used in niche cases where you know exactly what you're doing, + * e.g. when building an offline signing application. + * + * When you try to use online functionality with such a signer, an + * exception will be raised. + */ + public static async offline( + signer: OfflineSigner, + options: NibiSigningCosmWasmClientOptions = {} + ): Promise { + return new NibiSigningCosmWasmClient(undefined, signer, options) + } + + protected constructor( + cometClient: CometClient | undefined, + signer: OfflineSigner, + options: NibiSigningCosmWasmClientOptions + ) { + super(cometClient, options) + const { + registry = new Registry([...defaultStargateTypes, ...wasmTypes]), + aminoTypes = new AminoTypes({ + ...createDefaultAminoConverters(), + ...createWasmAminoConverters(), + }), + } = options + this.registry = registry + this.aminoTypes = aminoTypes + this.signer = signer + this.broadcastTimeoutMs = options.broadcastTimeoutMs + this.broadcastPollIntervalMs = options.broadcastPollIntervalMs + this.gasPrice = options.gasPrice + } + + public async simulate( + signerAddress: string, + messages: readonly EncodeObject[], + memo: string | undefined + ): Promise { + const anyMsgs = messages.map((m) => this.registry.encodeAsAny(m)) + const accountFromSigner = (await this.signer.getAccounts()).find( + (account) => account.address === signerAddress + ) + if (!accountFromSigner) { + throw new Error("Failed to retrieve account from signer") + } + const pubkey = encodeSecp256k1Pubkey(accountFromSigner.pubkey) + const { sequence } = await this.getSequence(signerAddress) + const { gasInfo } = await this.forceGetQueryClient().tx.simulate( + anyMsgs, + memo, + pubkey, + sequence + ) + assertDefined(gasInfo) + return Uint53.fromString(gasInfo?.gasUsed.toString()).toNumber() + } + + /** Uploads code and returns a receipt, including the code ID */ + public async upload( + senderAddress: string, + wasmCode: Uint8Array, + fee: FeeOption, + memo = "", + instantiatePermission?: AccessConfig + ): Promise { + const compressed = pako.gzip(wasmCode, { level: 9 }) + const storeCodeMsg: MsgStoreCodeEncodeObject = { + typeUrl: "/cosmwasm.wasm.v1.MsgStoreCode", + value: MsgStoreCode.fromPartial({ + sender: senderAddress, + wasmByteCode: compressed, + instantiatePermission, + }), + } + + // When uploading a contract, the simulation is only 1-2% away from the actual gas usage. + // So we have a smaller default gas multiplier than signAndBroadcast. + const usedFee = fee == "auto" ? 1.1 : fee + + const result = await this.signAndBroadcast( + senderAddress, + [storeCodeMsg], + usedFee, + memo + ) + if (isDeliverTxFailure(result)) { + throw new Error(createDeliverTxResponseErrorMessage(result)) + } + const codeIdAttr = findAttribute(result.events, "store_code", "code_id") + return { + checksum: toHex(sha256(wasmCode)), + originalSize: wasmCode.length, + compressedSize: compressed.length, + codeId: Number.parseInt(codeIdAttr.value, 10), + logs: logs.parseRawLog(result.rawLog), + height: result.height, + transactionHash: result.transactionHash, + events: result.events, + gasWanted: result.gasWanted, + gasUsed: result.gasUsed, + } + } + + public async instantiate( + senderAddress: string, + codeId: number, + msg: JsonObject, + label: string, + fee: FeeOption, + options: InstantiateOptions = {} + ): Promise { + const instantiateContractMsg: MsgInstantiateContractEncodeObject = { + typeUrl: "/cosmwasm.wasm.v1.MsgInstantiateContract", + value: MsgInstantiateContract.fromPartial({ + sender: senderAddress, + codeId: BigInt(new Uint53(codeId).toString()), + label: label, + msg: toUtf8(JSON.stringify(msg)), + funds: [...(options.funds || [])], + admin: options.admin, + }), + } + const result = await this.signAndBroadcast( + senderAddress, + [instantiateContractMsg], + fee, + options.memo + ) + if (isDeliverTxFailure(result)) { + throw new Error(createDeliverTxResponseErrorMessage(result)) + } + const contractAddressAttr = findAttribute( + result.events, + "instantiate", + "_contract_address" + ) + return { + contractAddress: contractAddressAttr.value, + logs: logs.parseRawLog(result.rawLog), + height: result.height, + transactionHash: result.transactionHash, + events: result.events, + gasWanted: result.gasWanted, + gasUsed: result.gasUsed, + } + } + + public async instantiate2( + senderAddress: string, + codeId: number, + salt: Uint8Array, + msg: JsonObject, + label: string, + fee: FeeOption, + options: InstantiateOptions = {} + ): Promise { + const instantiateContract2Msg: MsgInstantiateContract2EncodeObject = { + typeUrl: "/cosmwasm.wasm.v1.MsgInstantiateContract2", + value: MsgInstantiateContract2.fromPartial({ + sender: senderAddress, + codeId: BigInt(new Uint53(codeId).toString()), + label: label, + msg: toUtf8(JSON.stringify(msg)), + funds: [...(options.funds || [])], + admin: options.admin, + salt: salt, + fixMsg: false, + }), + } + const result = await this.signAndBroadcast( + senderAddress, + [instantiateContract2Msg], + fee, + options.memo + ) + if (isDeliverTxFailure(result)) { + throw new Error(createDeliverTxResponseErrorMessage(result)) + } + const contractAddressAttr = findAttribute( + result.events, + "instantiate", + "_contract_address" + ) + return { + contractAddress: contractAddressAttr.value, + logs: logs.parseRawLog(result.rawLog), + height: result.height, + transactionHash: result.transactionHash, + events: result.events, + gasWanted: result.gasWanted, + gasUsed: result.gasUsed, + } + } + + public async updateAdmin( + senderAddress: string, + contractAddress: string, + newAdmin: string, + fee: FeeOption, + memo = "" + ): Promise { + const updateAdminMsg: MsgUpdateAdminEncodeObject = { + typeUrl: "/cosmwasm.wasm.v1.MsgUpdateAdmin", + value: MsgUpdateAdmin.fromPartial({ + sender: senderAddress, + contract: contractAddress, + newAdmin: newAdmin, + }), + } + const result = await this.signAndBroadcast( + senderAddress, + [updateAdminMsg], + fee, + memo + ) + if (isDeliverTxFailure(result)) { + throw new Error(createDeliverTxResponseErrorMessage(result)) + } + return { + logs: logs.parseRawLog(result.rawLog), + height: result.height, + transactionHash: result.transactionHash, + events: result.events, + gasWanted: result.gasWanted, + gasUsed: result.gasUsed, + } + } + + public async clearAdmin( + senderAddress: string, + contractAddress: string, + fee: FeeOption, + memo = "" + ): Promise { + const clearAdminMsg: MsgClearAdminEncodeObject = { + typeUrl: "/cosmwasm.wasm.v1.MsgClearAdmin", + value: MsgClearAdmin.fromPartial({ + sender: senderAddress, + contract: contractAddress, + }), + } + const result = await this.signAndBroadcast( + senderAddress, + [clearAdminMsg], + fee, + memo + ) + if (isDeliverTxFailure(result)) { + throw new Error(createDeliverTxResponseErrorMessage(result)) + } + return { + logs: logs.parseRawLog(result.rawLog), + height: result.height, + transactionHash: result.transactionHash, + events: result.events, + gasWanted: result.gasWanted, + gasUsed: result.gasUsed, + } + } + + public async migrate( + senderAddress: string, + contractAddress: string, + codeId: number, + migrateMsg: JsonObject, + fee: FeeOption, + memo = "" + ): Promise { + const migrateContractMsg: MsgMigrateContractEncodeObject = { + typeUrl: "/cosmwasm.wasm.v1.MsgMigrateContract", + value: MsgMigrateContract.fromPartial({ + sender: senderAddress, + contract: contractAddress, + codeId: BigInt(new Uint53(codeId).toString()), + msg: toUtf8(JSON.stringify(migrateMsg)), + }), + } + const result = await this.signAndBroadcast( + senderAddress, + [migrateContractMsg], + fee, + memo + ) + if (isDeliverTxFailure(result)) { + throw new Error(createDeliverTxResponseErrorMessage(result)) + } + return { + logs: logs.parseRawLog(result.rawLog), + height: result.height, + transactionHash: result.transactionHash, + events: result.events, + gasWanted: result.gasWanted, + gasUsed: result.gasUsed, + } + } + + public async execute( + senderAddress: string, + contractAddress: string, + msg: JsonObject, + fee: FeeOption, + memo = "", + funds?: readonly Coin[] + ): Promise { + const instruction: ExecuteInstruction = { + contractAddress: contractAddress, + msg: msg, + funds: funds, + } + return this.executeMultiple(senderAddress, [instruction], fee, memo) + } + + /** + * Like `execute` but allows executing multiple messages in one transaction. + */ + public async executeMultiple( + senderAddress: string, + instructions: readonly ExecuteInstruction[], + fee: FeeOption, + memo = "" + ): Promise { + const msgs: MsgExecuteContractEncodeObject[] = instructions.map((i) => ({ + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: senderAddress, + contract: i.contractAddress, + msg: toUtf8(JSON.stringify(i.msg)), + funds: [...(i.funds || [])], + }), + })) + const result = await this.signAndBroadcast(senderAddress, msgs, fee, memo) + if (isDeliverTxFailure(result)) { + throw new Error(createDeliverTxResponseErrorMessage(result)) + } + return { + logs: logs.parseRawLog(result.rawLog), + height: result.height, + transactionHash: result.transactionHash, + events: result.events, + gasWanted: result.gasWanted, + gasUsed: result.gasUsed, + } + } + + public async sendTokens( + senderAddress: string, + recipientAddress: string, + amount: readonly Coin[], + fee: FeeOption, + memo = "" + ): Promise { + const sendMsg: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: senderAddress, + toAddress: recipientAddress, + amount: [...amount], + }, + } + return this.signAndBroadcast(senderAddress, [sendMsg], fee, memo) + } + + public async delegateTokens( + delegatorAddress: string, + validatorAddress: string, + amount: Coin, + fee: FeeOption, + memo = "" + ): Promise { + const delegateMsg: MsgDelegateEncodeObject = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: MsgDelegate.fromPartial({ + delegatorAddress: delegatorAddress, + validatorAddress, + amount, + }), + } + return this.signAndBroadcast(delegatorAddress, [delegateMsg], fee, memo) + } + + public async undelegateTokens( + delegatorAddress: string, + validatorAddress: string, + amount: Coin, + fee: FeeOption, + memo = "" + ): Promise { + const undelegateMsg: MsgUndelegateEncodeObject = { + typeUrl: "/cosmos.staking.v1beta1.MsgUndelegate", + value: MsgUndelegate.fromPartial({ + delegatorAddress: delegatorAddress, + validatorAddress, + amount, + }), + } + return this.signAndBroadcast(delegatorAddress, [undelegateMsg], fee, memo) + } + + public async withdrawRewards( + delegatorAddress: string, + validatorAddress: string, + fee: FeeOption, + memo = "" + ): Promise { + const withdrawDelegatorRewardMsg: MsgWithdrawDelegatorRewardEncodeObject = { + typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + value: MsgWithdrawDelegatorReward.fromPartial({ + delegatorAddress: delegatorAddress, + validatorAddress, + }), + } + return this.signAndBroadcast( + delegatorAddress, + [withdrawDelegatorRewardMsg], + fee, + memo + ) + } + + /** + * Creates a transaction with the given messages, fee, memo and timeout height. Then signs and broadcasts the transaction. + * + * @param signerAddress The address that will sign transactions using this instance. The signer must be able to sign with this address. + * @param messages + * @param fee + * @param memo + * @param timeoutHeight (optional) timeout height to prevent the tx from being committed past a certain height + */ + public async signAndBroadcast( + signerAddress: string, + messages: readonly EncodeObject[], + fee: FeeOption, + memo = "", + timeoutHeight?: bigint + ): Promise { + let usedFee: StdFee + if (fee == "auto" || typeof fee === "number") { + assertDefined( + this.gasPrice, + "Gas price must be set in the client options when auto gas is used." + ) + const gasEstimation = await this.simulate(signerAddress, messages, memo) + const multiplier = + typeof fee === "number" ? fee : this.defaultGasMultiplier + usedFee = calculateFee( + Math.round(gasEstimation * multiplier), + this.gasPrice + ) + } else { + usedFee = fee + } + const txRaw = await this.sign( + signerAddress, + messages, + usedFee, + memo, + undefined, + timeoutHeight + ) + const txBytes = TxRaw.encode(txRaw).finish() + return this.broadcastTx( + txBytes, + this.broadcastTimeoutMs, + this.broadcastPollIntervalMs + ) + } + + /** + * Creates a transaction with the given messages, fee, memo and timeout height. Then signs and broadcasts the transaction. + * + * This method is useful if you want to send a transaction in broadcast, + * without waiting for it to be placed inside a block, because for example + * I would like to receive the hash to later track the transaction with another tool. + * + * @param signerAddress The address that will sign transactions using this instance. The signer must be able to sign with this address. + * @param messages + * @param fee + * @param memo + * @param timeoutHeight (optional) timeout height to prevent the tx from being committed past a certain height + * + * @returns Returns the hash of the transaction + */ + public async signAndBroadcastSync( + signerAddress: string, + messages: readonly EncodeObject[], + fee: FeeOption, + memo = "", + timeoutHeight?: bigint + ): Promise { + let usedFee: StdFee + if (fee == "auto" || typeof fee === "number") { + assertDefined( + this.gasPrice, + "Gas price must be set in the client options when auto gas is used." + ) + const gasEstimation = await this.simulate(signerAddress, messages, memo) + const multiplier = + typeof fee === "number" ? fee : this.defaultGasMultiplier + usedFee = calculateFee( + Math.round(gasEstimation * multiplier), + this.gasPrice + ) + } else { + usedFee = fee + } + const txRaw = await this.sign( + signerAddress, + messages, + usedFee, + memo, + undefined, + timeoutHeight + ) + const txBytes = TxRaw.encode(txRaw).finish() + return this.broadcastTxSync(txBytes) + } + + public async sign( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + explicitSignerData?: SignerData, + timeoutHeight?: bigint + ): Promise { + let signerData: SignerData + if (explicitSignerData) { + signerData = explicitSignerData + } else { + const { accountNumber, sequence } = await this.getSequence(signerAddress) + const chainId = await this.getChainId() + signerData = { + accountNumber: accountNumber, + sequence: sequence, + chainId: chainId, + } + } + + return isOfflineDirectSigner(this.signer) + ? this.signDirect( + signerAddress, + messages, + fee, + memo, + signerData, + timeoutHeight + ) + : this.signAmino( + signerAddress, + messages, + fee, + memo, + signerData, + timeoutHeight + ) + } + + private async signAmino( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + { accountNumber, sequence, chainId }: SignerData, + timeoutHeight?: bigint + ): Promise { + assert(!isOfflineDirectSigner(this.signer)) + const accountFromSigner = (await this.signer.getAccounts()).find( + (account) => account.address === signerAddress + ) + if (!accountFromSigner) { + throw new Error("Failed to retrieve account from signer") + } + const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)) + const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON + const msgs = messages.map((msg) => this.aminoTypes.toAmino(msg)) + const signDoc = makeSignDocAmino( + msgs, + fee, + chainId, + memo, + accountNumber, + sequence, + timeoutHeight + ) + const { signature, signed } = await this.signer.signAmino( + signerAddress, + signDoc + ) + const signedTxBody: TxBodyEncodeObject = { + typeUrl: "/cosmos.tx.v1beta1.TxBody", + value: { + messages: signed.msgs.map((msg) => this.aminoTypes.fromAmino(msg)), + memo: signed.memo, + timeoutHeight: timeoutHeight, + }, + } + const signedTxBodyBytes = this.registry.encode(signedTxBody) + const signedGasLimit = Int53.fromString(signed.fee.gas).toNumber() + const signedSequence = Int53.fromString(signed.sequence).toNumber() + const signedAuthInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence: signedSequence }], + signed.fee.amount, + signedGasLimit, + signed.fee.granter, + signed.fee.payer, + signMode + ) + return TxRaw.fromPartial({ + bodyBytes: signedTxBodyBytes, + authInfoBytes: signedAuthInfoBytes, + signatures: [fromBase64(signature.signature)], + }) + } + + private async signDirect( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + { accountNumber, sequence, chainId }: SignerData, + timeoutHeight?: bigint + ): Promise { + assert(isOfflineDirectSigner(this.signer)) + const accountFromSigner = (await this.signer.getAccounts()).find( + (account) => account.address === signerAddress + ) + if (!accountFromSigner) { + throw new Error("Failed to retrieve account from signer") + } + const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)) + const txBody: TxBodyEncodeObject = { + typeUrl: "/cosmos.tx.v1beta1.TxBody", + value: { + messages: messages, + memo: memo, + timeoutHeight: timeoutHeight, + }, + } + const txBodyBytes = this.registry.encode(txBody) + const gasLimit = Int53.fromString(fee.gas).toNumber() + const authInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence }], + fee.amount, + gasLimit, + fee.granter, + fee.payer + ) + const signDoc = makeSignDoc( + txBodyBytes, + authInfoBytes, + chainId, + accountNumber + ) + const { signature, signed } = await this.signer.signDirect( + signerAddress, + signDoc + ) + return TxRaw.fromPartial({ + bodyBytes: signed.bodyBytes, + authInfoBytes: signed.authInfoBytes, + signatures: [fromBase64(signature.signature)], + }) + } +} diff --git a/src/sdk/tx/account.test.ts b/src/sdk/tx/account.test.ts new file mode 100644 index 00000000..d392b404 --- /dev/null +++ b/src/sdk/tx/account.test.ts @@ -0,0 +1,105 @@ +import { accountFromEthAccount, accountFromNibiru } from "./account" +import { EthAccount } from "src/protojs/eth/types/v1/account" +import { Any } from "src/protojs/google/protobuf/any" +import Long from "long" +import * as cosmjs from "@cosmjs/stargate" +import { decodeOptionalPubkey } from "@cosmjs/proto-signing" +import { BaseAccount } from "src/protojs/cosmos/auth/v1beta1/auth" + +// Mock decodeOptionalPubkey +jest.mock("@cosmjs/proto-signing", () => ({ + decodeOptionalPubkey: jest.fn(), +})) + +const mockedDecodeOptionalPubkey = decodeOptionalPubkey as jest.Mock + +describe("accountFromEthAccount", () => { + it("should throw an error if baseAccount is undefined", () => { + const baseAccount: BaseAccount = undefined as unknown as BaseAccount + + expect(() => accountFromEthAccount(baseAccount)).toThrow() + }) + + it("should return a valid account when baseAccount is defined", () => { + const baseAccount: BaseAccount = { + address: "nibi1testaddress", + pubKey: { + typeUrl: "/cosmos.crypto.secp256k1.PubKey", + value: new Uint8Array([1, 2, 3]), + }, + accountNumber: Long.fromNumber(123), + sequence: Long.fromNumber(1), + } + + mockedDecodeOptionalPubkey.mockReturnValue({ + typeUrl: "/cosmos.crypto.secp256k1.PubKey", + value: new Uint8Array([1, 2, 3]), + }) + + const account = accountFromEthAccount(baseAccount) + + expect(account.address).toBe("nibi1testaddress") + expect(account.pubkey).toEqual({ + typeUrl: "/cosmos.crypto.secp256k1.PubKey", + value: new Uint8Array([1, 2, 3]), + }) + expect(account.accountNumber).toEqual(123) + expect(account.sequence).toEqual(1) + }) +}) + +describe("accountFromNibiru", () => { + it("should parse EthAccount typeUrl and return valid account", () => { + const input: Any = { + typeUrl: "/eth.types.v1.EthAccount", + value: EthAccount.encode({ + baseAccount: { + address: "nibi1testaddress", + pubKey: { + typeUrl: "/cosmos.crypto.secp256k1.PubKey", + value: new Uint8Array([4, 5, 6]), + }, + accountNumber: Long.fromNumber(456), + sequence: Long.fromNumber(2), + }, + codeHash: "", + }).finish(), + } + + mockedDecodeOptionalPubkey.mockReturnValue({ + typeUrl: "/cosmos.crypto.secp256k1.PubKey", + value: new Uint8Array([4, 5, 6]), + }) + + const account = accountFromNibiru(input) + + expect(account.address).toBe("nibi1testaddress") + expect(account.pubkey).toEqual({ + typeUrl: "/cosmos.crypto.secp256k1.PubKey", + value: new Uint8Array([4, 5, 6]), + }) + expect(account.accountNumber).toEqual(456) + expect(account.sequence).toEqual(2) + }) + + it("should handle non-EthAccount typeUrl by calling accountFromAny", () => { + const mockAccountFromAny = jest + .spyOn(cosmjs, "accountFromAny") + .mockReturnValue({ + address: "nibi1otheraddress", + pubkey: null, + accountNumber: 789, + sequence: 3, + }) + + const input: Any = { + typeUrl: "/other.types.v1.Account", + value: new Uint8Array([7, 8, 9]), + } + + const account = accountFromNibiru(input) + + expect(account.address).toBe("nibi1otheraddress") + expect(mockAccountFromAny).toHaveBeenCalledWith(input) + }) +}) diff --git a/src/sdk/tx/account.ts b/src/sdk/tx/account.ts new file mode 100644 index 00000000..75d2fe05 --- /dev/null +++ b/src/sdk/tx/account.ts @@ -0,0 +1,42 @@ +import { decodeOptionalPubkey } from "@cosmjs/proto-signing" +import { Account, accountFromAny, AccountParser } from "@cosmjs/stargate" +import { EthAccount } from "src/protojs/eth/types/v1/account" +import { Any } from "src/protojs/google/protobuf/any" +import { assert } from "@cosmjs/utils" +import { BaseAccount } from "src/protojs/cosmos/auth/v1beta1/auth" + +/** + * Converts an EthAccount to a general Cosmos Account object. + * + * @param {EthAccount} ethAccount - The EthAccount object containing the account's base information. + * @returns {Account} The Cosmos account object. + */ +export const accountFromEthAccount = ({ + address, + pubKey, + accountNumber, + sequence, +}: BaseAccount): Account => ({ + address, + pubkey: decodeOptionalPubkey(pubKey), + accountNumber: accountNumber.toNumber(), + sequence: sequence.toNumber(), +}) + +/** + * Parses an account input into a Cosmos account. Handles both EthAccount and other standard accounts. + * + * @param {Any} input - The input account information, containing the typeUrl and value. + * @returns {Account} Parsed account object. + */ +export const accountFromNibiru: AccountParser = (input: Any): Account => { + const { typeUrl, value } = input + + if (typeUrl === "/eth.types.v1.EthAccount") { + const baseAccount = EthAccount.decode(value).baseAccount + assert(baseAccount) + return accountFromEthAccount(baseAccount) + } + + return accountFromAny(input) +} diff --git a/src/sdk/tx/index.ts b/src/sdk/tx/index.ts index 3650d68b..23e3833e 100644 --- a/src/sdk/tx/index.ts +++ b/src/sdk/tx/index.ts @@ -2,6 +2,7 @@ * @file Automatically generated by barrelsby. */ +export * from "./account" export * from "./event" export * from "./signer" export * from "./txClient" diff --git a/src/sdk/tx/txClient.ts b/src/sdk/tx/txClient.ts index 38b718b0..1f1c10fd 100644 --- a/src/sdk/tx/txClient.ts +++ b/src/sdk/tx/txClient.ts @@ -12,12 +12,13 @@ import { SigningStargateClientOptions, } from "@cosmjs/stargate" import { Tendermint37Client } from "@cosmjs/tendermint-rpc" -import { - SigningCosmWasmClient, - SigningCosmWasmClientOptions, - setupWasmExtension, -} from "@cosmjs/cosmwasm-stargate" +import { setupWasmExtension } from "@cosmjs/cosmwasm-stargate" import { NibiruExtensions, setupNibiruExtension } from ".." +import { accountFromNibiru } from "./account" +import { + NibiSigningCosmWasmClient, + NibiSigningCosmWasmClientOptions, +} from "../core/signingcosmwasmclient" export const nibiruRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [ ...defaultRegistryTypes, @@ -25,13 +26,13 @@ export const nibiruRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [ export class NibiruTxClient extends SigningStargateClient { public readonly nibiruExtensions: NibiruExtensions - public readonly wasmClient: SigningCosmWasmClient + public readonly wasmClient: NibiSigningCosmWasmClient protected constructor( tmClient: Tendermint37Client, signer: OfflineSigner, options: SigningStargateClientOptions, - wasm: SigningCosmWasmClient + wasm: NibiSigningCosmWasmClient ) { super(tmClient, signer, options) this.wasmClient = wasm @@ -51,14 +52,15 @@ export class NibiruTxClient extends SigningStargateClient { endpoint: string, signer: OfflineSigner, options: SigningStargateClientOptions = {}, - wasmOptions: SigningCosmWasmClientOptions = {} + wasmOptions: NibiSigningCosmWasmClientOptions = {} ): Promise { const tmClient = await Tendermint37Client.connect(endpoint) - const wasmClient = await SigningCosmWasmClient.connectWithSigner( + const wasmClient = await NibiSigningCosmWasmClient.connectWithSigner( endpoint, signer, { gasPrice: GasPrice.fromString("0.025unibi"), + accountParser: accountFromNibiru, ...wasmOptions, } ) @@ -69,6 +71,7 @@ export class NibiruTxClient extends SigningStargateClient { registry: new Registry(nibiruRegistryTypes), gasPrice: GasPrice.fromString("0.025unibi"), broadcastPollIntervalMs: 1_000, // 1 second poll times + accountParser: accountFromNibiru, ...options, }, wasmClient diff --git a/yarn.lock b/yarn.lock index 8ab48d9e..bfd407e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2492,6 +2492,11 @@ resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/pako@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/pako/-/pako-2.0.3.tgz#b6993334f3af27c158f3fe0dfeeba987c578afb1" + integrity sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" @@ -7255,7 +7260,7 @@ pacote@^13.0.3, pacote@^13.6.1, pacote@^13.6.2: ssri "^9.0.0" tar "^6.1.11" -pako@^2.0.2: +pako@^2.0.2, pako@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz" integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== @@ -8245,7 +8250,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8277,7 +8291,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8930,7 +8951,7 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8948,6 +8969,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"