Skip to content

Commit 98387c8

Browse files
committed
feat: Enabled simulation + recommended transaction batching
1 parent 11d0b86 commit 98387c8

File tree

9 files changed

+753
-36
lines changed

9 files changed

+753
-36
lines changed

cjs/src/querier.ts

+15
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
import { QueryClient } from '@cosmjs/stargate-cjs';
22
import { Tendermint34Client, Tendermint37Client } from '@cosmjs/tendermint-rpc-cjs';
33
import { QueryExtensionSetup, CheqdExtensions } from './types';
4+
import { ConsensusParams } from '@cosmjs/tendermint-rpc-cjs';
45

56
export class CheqdQuerier extends QueryClient {
67
constructor(tmClient: Tendermint37Client | Tendermint34Client) {
78
super(tmClient);
89
}
910

11+
static async getConsensusParameters(url: string): Promise<ConsensusParams | undefined> {
12+
// connect to comet rpc
13+
const cometClient = await Tendermint37Client.connect(url);
14+
15+
// get block results
16+
const result = await cometClient.blockResults();
17+
18+
// disconnect comet client
19+
cometClient.disconnect();
20+
21+
// return consensus parameters
22+
return result.consensusUpdates;
23+
}
24+
1025
static async connect(url: string): Promise<CheqdQuerier> {
1126
const tmClient = await Tendermint37Client.connect(url);
1227
return new CheqdQuerier(tmClient);

cjs/src/signer.ts

+180-9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
SigningStargateClientOptions,
1515
calculateFee,
1616
SignerData,
17+
createProtobufRpcClient,
1718
} from '@cosmjs/stargate-cjs';
1819
import { Tendermint37Client } from '@cosmjs/tendermint-rpc-cjs';
1920
import { createDefaultCheqdRegistry } from './registry';
@@ -25,17 +26,34 @@ import {
2526
VerificationMethod,
2627
DidDoc,
2728
} from '@cheqd/ts-proto-cjs/cheqd/did/v2';
28-
import { DIDDocument, DidStdFee, ISignInputs, TSignerAlgo, VerificationMethods } from './types';
29+
import {
30+
DIDDocument,
31+
DidStdFee,
32+
ISignInputs,
33+
MessageBatch,
34+
TSignerAlgo,
35+
TxExtension,
36+
VerificationMethods,
37+
} from './types';
2938
import { base64ToBytes, EdDSASigner, hexToBytes, Signer, ES256Signer, ES256KSigner } from 'did-jwt-cjs';
3039
import { assert, assertDefined } from '@cosmjs/utils-cjs';
31-
import { encodeSecp256k1Pubkey } from '@cosmjs/amino-cjs';
40+
import { encodeSecp256k1Pubkey, Pubkey } from '@cosmjs/amino-cjs';
3241
import { Int53 } from '@cosmjs/math-cjs';
3342
import { fromBase64 } from '@cosmjs/encoding-cjs';
3443
import { AuthInfo, SignerInfo, TxRaw } from 'cosmjs-types-cjs/cosmos/tx/v1beta1/tx';
3544
import { SignMode } from 'cosmjs-types-cjs/cosmos/tx/signing/v1beta1/signing';
3645
import { Any } from 'cosmjs-types-cjs/google/protobuf/any';
3746
import { Coin } from 'cosmjs-types-cjs/cosmos/base/v1beta1/coin';
3847
import Long from 'long-cjs';
48+
import { CheqdQuerier } from './querier';
49+
import { Uint53 } from '@cosmjs/math-cjs';
50+
import {
51+
GetTxResponse,
52+
ServiceClientImpl,
53+
SimulateRequest,
54+
SimulateResponse,
55+
} from 'cosmjs-types-cjs/cosmos/tx/v1beta1/service';
56+
import { Tx, TxBody, Fee } from 'cosmjs-types/cosmos/tx/v1beta1/tx.js';
3957

4058
export function calculateDidFee(gasLimit: number, gasPrice: string | GasPrice): DidStdFee {
4159
return calculateFee(gasLimit, gasPrice);
@@ -78,27 +96,34 @@ export class CheqdSigningStargateClient extends SigningStargateClient {
7896
private didSigners: TSignerAlgo = {};
7997
private readonly _gasPrice: GasPrice | undefined;
8098
private readonly _signer: OfflineSigner;
99+
private readonly endpoint?: string;
81100

82101
public static async connectWithSigner(
83102
endpoint: string | HttpEndpoint,
84103
signer: OfflineSigner,
85-
options?: SigningStargateClientOptions | undefined
104+
options?: (SigningStargateClientOptions & { endpoint?: string }) | undefined
86105
): Promise<CheqdSigningStargateClient> {
87-
const tmClient = await Tendermint37Client.connect(endpoint);
88-
return new CheqdSigningStargateClient(tmClient, signer, {
106+
const cometClient = await Tendermint37Client.connect(endpoint);
107+
return new CheqdSigningStargateClient(cometClient, signer, {
89108
registry: options?.registry ? options.registry : createDefaultCheqdRegistry(),
109+
endpoint: options?.endpoint
110+
? typeof options.endpoint === 'string'
111+
? options.endpoint
112+
: (options.endpoint as HttpEndpoint).url
113+
: undefined,
90114
...options,
91115
});
92116
}
93117

94118
constructor(
95-
tmClient: Tendermint37Client | undefined,
119+
cometClient: Tendermint37Client | undefined,
96120
signer: OfflineSigner,
97-
options: SigningStargateClientOptions = {}
121+
options: SigningStargateClientOptions & { endpoint?: string } = {}
98122
) {
99-
super(tmClient, signer, options);
123+
super(cometClient, signer, options);
100124
this._signer = signer;
101-
if (options.gasPrice) this._gasPrice = options.gasPrice;
125+
this._gasPrice = options.gasPrice;
126+
this.endpoint = options.endpoint;
102127
}
103128

104129
async signAndBroadcast(
@@ -181,6 +206,152 @@ export class CheqdSigningStargateClient extends SigningStargateClient {
181206
});
182207
}
183208

209+
async simulate(
210+
signerAddress: string,
211+
messages: readonly EncodeObject[],
212+
memo: string | undefined
213+
): Promise<number> {
214+
if (!this.endpoint) {
215+
throw new Error('querier: endpoint is not set');
216+
}
217+
const querier = await CheqdQuerier.connect(this.endpoint);
218+
const anyMsgs = messages.map((msg) => this.registry.encodeAsAny(msg));
219+
const accountFromSigner = (await this._signer.getAccounts()).find(
220+
(account) => account.address === signerAddress
221+
);
222+
if (!accountFromSigner) {
223+
throw new Error('Failed to retrieve account from signer');
224+
}
225+
const pubkey = encodeSecp256k1Pubkey(accountFromSigner.pubkey);
226+
const { sequence } = await this.getSequence(signerAddress);
227+
const gasLimit = (await CheqdQuerier.getConsensusParameters(this.endpoint))!.block.maxGas;
228+
const { gasInfo } = await (
229+
await this.constructSimulateExtension(querier)
230+
).tx.simulate(anyMsgs, memo, pubkey, signerAddress, sequence, gasLimit);
231+
assertDefined(gasInfo);
232+
return Uint53.fromString(gasInfo.gasUsed.toString()).toNumber();
233+
}
234+
235+
async constructSimulateExtension(querier: CheqdQuerier): Promise<TxExtension> {
236+
// setup rpc client
237+
const rpc = createProtobufRpcClient(querier);
238+
239+
// setup query tx query service
240+
const queryService = new ServiceClientImpl(rpc);
241+
242+
// setup + return tx extension
243+
return {
244+
tx: {
245+
getTx: async (txId: string): Promise<GetTxResponse> => {
246+
// construct request
247+
const request = { hash: txId };
248+
249+
// query + return tx
250+
return await queryService.GetTx(request);
251+
},
252+
simulate: async (
253+
messages: readonly Any[],
254+
memo: string | undefined,
255+
signer: Pubkey,
256+
signerAddress: string,
257+
sequence: number,
258+
gasLimit: number
259+
): Promise<SimulateResponse> => {
260+
// encode public key
261+
const publicKey = encodePubkey(signer);
262+
263+
// construct max gas limit
264+
const maxGasLimit = Int53.fromString(gasLimit.toString()).toNumber();
265+
266+
// construct unsigned tx
267+
const tx = Tx.fromPartial({
268+
body: TxBody.fromPartial({
269+
messages: Array.from(messages),
270+
memo,
271+
}),
272+
authInfo: AuthInfo.fromPartial({
273+
fee: Fee.fromPartial({
274+
amount: [],
275+
gasLimit: maxGasLimit,
276+
payer: signerAddress,
277+
}),
278+
signerInfos: [
279+
{
280+
publicKey,
281+
modeInfo: {
282+
single: { mode: SignMode.SIGN_MODE_DIRECT },
283+
},
284+
sequence: sequence,
285+
},
286+
],
287+
}),
288+
signatures: [new Uint8Array()],
289+
});
290+
291+
// construct request
292+
const request = SimulateRequest.fromPartial({
293+
txBytes: Tx.encode(tx).finish(),
294+
});
295+
296+
// query + return simulation response
297+
return await queryService.Simulate(request);
298+
},
299+
},
300+
};
301+
}
302+
303+
async batchMessages(
304+
messages: readonly EncodeObject[],
305+
signerAddress: string,
306+
memo?: string,
307+
maxGasLimit: number = 30000000 // default gas limit, use consensus params if available
308+
): Promise<MessageBatch> {
309+
// simulate
310+
const gasEstimates = await Promise.all(
311+
messages.map(async (message) => this.simulate(signerAddress, [message], memo))
312+
);
313+
314+
// batch messages
315+
const { batches, gasPerBatch, currentBatch, currentBatchGas } = gasEstimates.reduce(
316+
(acc, gasUsed, index) => {
317+
// finalise current batch, if limit is surpassed
318+
if (acc.currentBatchGas + gasUsed > maxGasLimit) {
319+
return {
320+
batches: [...acc.batches, acc.currentBatch],
321+
gasPerBatch: [...acc.gasPerBatch, acc.currentBatchGas],
322+
currentBatch: [messages[index]], // Start a new batch with the current message
323+
currentBatchGas: gasUsed, // Reset the gas counter for the new batch
324+
};
325+
}
326+
327+
// otherwise, add to current batch
328+
return {
329+
batches: acc.batches,
330+
gasPerBatch: acc.gasPerBatch,
331+
currentBatch: [...acc.currentBatch, messages[index]],
332+
currentBatchGas: acc.currentBatchGas + gasUsed,
333+
};
334+
},
335+
{
336+
batches: [] as EncodeObject[][],
337+
gasPerBatch: [] as number[],
338+
currentBatch: [] as EncodeObject[],
339+
currentBatchGas: 0,
340+
}
341+
);
342+
343+
// push final batch to batches, if not empty + return
344+
return currentBatch.length > 0
345+
? {
346+
batches: [...batches, currentBatch],
347+
gas: [...gasPerBatch, currentBatchGas],
348+
}
349+
: {
350+
batches,
351+
gas: gasPerBatch,
352+
};
353+
}
354+
184355
async checkDidSigners(verificationMethods: Partial<VerificationMethod>[] = []): Promise<TSignerAlgo> {
185356
if (verificationMethods.length === 0) {
186357
throw new Error('No verification methods provided');

cjs/src/types.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ import {
33
VerificationMethod as ProtobufVerificationMethod,
44
} from '@cheqd/ts-proto-cjs/cheqd/did/v2';
55
import { CheqdSDK } from './index';
6-
import { Coin } from '@cosmjs/proto-signing-cjs';
6+
import { Coin, EncodeObject } from '@cosmjs/proto-signing-cjs';
77
import { Signer } from 'did-jwt-cjs';
88
import { QueryClient } from '@cosmjs/stargate-cjs';
99
import { DIDDocument, DIDResolutionResult } from 'did-resolver-cjs';
1010
import { DidExtension } from './modules/did';
1111
import { ResourceExtension } from './modules/resource';
1212
import { FeemarketExtension } from './modules/feemarket';
1313
import { FeeabstractionExtension } from './modules/feeabstraction';
14+
import { GetTxResponse, SimulateResponse } from 'cosmjs-types-cjs/cosmos/tx/v1beta1/service';
15+
import { Any } from 'cosmjs-types-cjs/google/protobuf/any';
16+
import { Pubkey } from '@cosmjs/amino-cjs';
1417
export { DIDDocument, VerificationMethod, Service, ServiceEndpoint, JsonWebKey } from 'did-resolver-cjs';
1518

1619
export enum CheqdNetwork {
@@ -26,6 +29,20 @@ export type CheqdExtension<K extends string, V = any> = {
2629

2730
export type CheqdExtensions = DidExtension | ResourceExtension | FeemarketExtension | FeeabstractionExtension;
2831

32+
export interface TxExtension {
33+
readonly tx: {
34+
getTx: (txId: string) => Promise<GetTxResponse>;
35+
simulate: (
36+
messages: readonly Any[],
37+
memo: string | undefined,
38+
signer: Pubkey,
39+
signerAddress: string,
40+
sequence: number,
41+
gasLimit: number
42+
) => Promise<SimulateResponse>;
43+
};
44+
}
45+
2946
export interface IModuleMethod {
3047
(...args: any[]): Promise<any>;
3148
}
@@ -52,6 +69,11 @@ export type AuthenticationValidationResult = {
5269
previousDidDocument?: DIDDocument;
5370
};
5471

72+
export type MessageBatch = {
73+
readonly batches: EncodeObject[][];
74+
readonly gas: number[];
75+
};
76+
5577
export enum VerificationMethods {
5678
Ed255192020 = 'Ed25519VerificationKey2020',
5779
Ed255192018 = 'Ed25519VerificationKey2018',

0 commit comments

Comments
 (0)