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/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/txClient.ts b/src/sdk/tx/txClient.ts index 838df533..1f1c10fd 100644 --- a/src/sdk/tx/txClient.ts +++ b/src/sdk/tx/txClient.ts @@ -12,13 +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, @@ -26,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 @@ -52,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, } ) 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"