@cheqd/sdk
Version:
A TypeScript SDK built with CosmJS to interact with the cheqd network ledger
456 lines • 22.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CheqdSigningStargateClient = void 0;
exports.calculateDidFee = calculateDidFee;
exports.makeSignerInfos = makeSignerInfos;
exports.makeDidAuthInfoBytes = makeDidAuthInfoBytes;
const proto_signing_cjs_1 = require("@cosmjs/proto-signing-cjs");
const stargate_cjs_1 = require("@cosmjs/stargate-cjs");
const tendermint_rpc_cjs_1 = require("@cosmjs/tendermint-rpc-cjs");
const registry_1 = require("./registry");
const v2_1 = require("@cheqd/ts-proto-cjs/cheqd/did/v2");
const types_1 = require("./types");
const did_jwt_cjs_1 = require("did-jwt-cjs");
const utils_cjs_1 = require("@cosmjs/utils-cjs");
const amino_cjs_1 = require("@cosmjs/amino-cjs");
const math_cjs_1 = require("@cosmjs/math-cjs");
const encoding_cjs_1 = require("@cosmjs/encoding-cjs");
const tx_1 = require("cosmjs-types-cjs/cosmos/tx/v1beta1/tx");
const signing_1 = require("cosmjs-types-cjs/cosmos/tx/signing/v1beta1/signing");
const long_cjs_1 = __importDefault(require("long-cjs"));
const querier_1 = require("./querier");
const math_cjs_2 = require("@cosmjs/math-cjs");
const service_1 = require("cosmjs-types-cjs/cosmos/tx/v1beta1/service");
const tx_js_1 = require("cosmjs-types/cosmos/tx/v1beta1/tx.js");
function calculateDidFee(gasLimit, gasPrice) {
return (0, stargate_cjs_1.calculateFee)(gasLimit, gasPrice);
}
function makeSignerInfos(signers, signMode) {
return signers.map(({ pubkey, sequence }) => ({
publicKey: pubkey,
modeInfo: {
single: { mode: signMode },
},
sequence: long_cjs_1.default.fromNumber(sequence),
}));
}
function makeDidAuthInfoBytes(signers, feeAmount, gasLimit, feePayer, signMode = signing_1.SignMode.SIGN_MODE_DIRECT) {
const authInfo = {
signerInfos: makeSignerInfos(signers, signMode),
fee: {
amount: [...feeAmount],
gasLimit: long_cjs_1.default.fromNumber(gasLimit),
payer: feePayer,
},
};
return tx_1.AuthInfo.encode(tx_1.AuthInfo.fromPartial(authInfo)).finish();
}
class CheqdSigningStargateClient extends stargate_cjs_1.SigningStargateClient {
didSigners = {};
_gasPrice;
_signer;
endpoint;
static maxGasLimit = Number.MAX_SAFE_INTEGER;
static async connectWithSigner(endpoint, signer, options) {
const cometClient = await tendermint_rpc_cjs_1.Tendermint37Client.connect(endpoint);
return new CheqdSigningStargateClient(cometClient, signer, {
registry: options?.registry ? options.registry : (0, registry_1.createDefaultCheqdRegistry)(),
endpoint: options?.endpoint
? typeof options.endpoint === 'string'
? options.endpoint
: options.endpoint.url
: undefined,
...options,
});
}
constructor(cometClient, signer, options = {}) {
super(cometClient, signer, options);
this._signer = signer;
this._gasPrice = options.gasPrice;
this.endpoint = options.endpoint;
}
async signAndBroadcast(signerAddress, messages, fee, memo = '') {
let usedFee;
if (fee == 'auto' || typeof fee === 'number') {
(0, utils_cjs_1.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 : 1.3;
usedFee = calculateDidFee(Math.round(gasEstimation * multiplier), this._gasPrice);
usedFee.payer = signerAddress;
}
else {
usedFee = fee;
(0, utils_cjs_1.assertDefined)(usedFee.payer, 'Payer address must be set when fee is not auto.');
signerAddress = usedFee.payer;
}
const txRaw = await this.sign(signerAddress, messages, usedFee, memo);
const txBytes = tx_1.TxRaw.encode(txRaw).finish();
return this.broadcastTx(txBytes, this.broadcastTimeoutMs, this.broadcastPollIntervalMs);
}
async sign(signerAddress, messages, fee, memo, explicitSignerData) {
let 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 this._signDirect(signerAddress, messages, fee, memo, signerData);
}
async _signDirect(signerAddress, messages, fee, memo, { accountNumber, sequence, chainId }) {
(0, utils_cjs_1.assert)((0, proto_signing_cjs_1.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 = (0, proto_signing_cjs_1.encodePubkey)((0, amino_cjs_1.encodeSecp256k1Pubkey)(accountFromSigner.pubkey));
const txBodyEncodeObject = {
typeUrl: '/cosmos.tx.v1beta1.TxBody',
value: {
messages: messages,
memo: memo,
},
};
const txBodyBytes = this.registry.encode(txBodyEncodeObject);
const gasLimit = math_cjs_1.Int53.fromString(fee.gas).toNumber();
const authInfoBytes = makeDidAuthInfoBytes([{ pubkey, sequence }], fee.amount, gasLimit, fee.payer);
const signDoc = (0, proto_signing_cjs_1.makeSignDoc)(txBodyBytes, authInfoBytes, chainId, accountNumber);
const { signature, signed } = await this._signer.signDirect(signerAddress, signDoc);
return tx_1.TxRaw.fromPartial({
bodyBytes: signed.bodyBytes,
authInfoBytes: signed.authInfoBytes,
signatures: [(0, encoding_cjs_1.fromBase64)(signature.signature)],
});
}
/**
* Broadcasts a signed transaction to the network and monitors its inclusion in a block,
* with support for retrying on failure and graceful timeout handling.
*
* ## Optimizations over base implementation:
* - Implements a retry policy (`maxRetries`, default: 3) to handle transient broadcast errors.
* - Prevents double spend by reusing the exact same signed transaction (immutable `Uint8Array`).
* - Tracks and returns the last known transaction hash (`txId`), even in the case of timeout or failure.
* - Throws a `TimeoutError` if the transaction is not found within `timeoutMs`, attaching the `txHash` if known.
* - Polling frequency and timeout are customizable via `pollIntervalMs` and `timeoutMs` parameters.
*
* @param tx - The signed transaction bytes to broadcast.
* @param timeoutMs - Maximum duration (in milliseconds) to wait for block inclusion. Defaults to 60,000 ms.
* @param pollIntervalMs - Polling interval (in milliseconds) when checking for transaction inclusion. Defaults to 3,000 ms.
* @param maxRetries - Maximum number of times to retry `broadcastTxSync` on failure. Defaults to 3.
*
* @returns A `Promise` that resolves to `DeliverTxResponse` upon successful inclusion in a block.
* @throws `BroadcastTxError` if the transaction is rejected by the node during CheckTx.
* @throws `TimeoutError` if the transaction is not found on-chain within the timeout window. Includes `txHash` if available.
*/
async broadcastTx(tx, timeoutMs = 60_000, pollIntervalMs = 3_000, maxRetries = 3) {
// define polling callback
const pollForTx = async (txId, startTime) => {
// define immutable timeout
const timedOut = Date.now() - startTime > timeoutMs;
// check if timed out
if (timedOut) {
// if so, throw timeout error with txId (or transaction hash)
throw new stargate_cjs_1.TimeoutError(`Transaction with ID ${txId} was submitted but was not yet found on the chain. Waited ${timeoutMs / 1000} seconds.`, txId);
}
// otherwise, poll for tx
await (0, utils_cjs_1.sleep)(pollIntervalMs);
// query for tx
const result = await this.getTx(txId);
// return result if found, otherwise poll again
return result
? {
code: result.code,
height: result.height,
txIndex: result.txIndex,
events: result.events,
rawLog: result.rawLog,
transactionHash: txId,
gasUsed: result.gasUsed,
gasWanted: result.gasWanted,
}
: pollForTx(txId, startTime);
};
// define immutable array of errors
const errors = [];
// define last known transaction hash
let lastKnownTxHash;
// attempt to broadcast tx until maxRetries or tx is found
for (const attempt of Array.from({ length: maxRetries }, (_, i) => i + 1)) {
try {
// broadcast tx
const txId = await this.broadcastTxSync(tx);
// set last known transaction hash
lastKnownTxHash = txId;
// recompute start time
const startTime = Date.now();
// poll for tx
const result = await pollForTx(txId, startTime);
// if successful, return result
return result;
}
catch (error) {
// if error, push to errors array
errors.push(error);
// define last error
const lastError = error;
// if error is not a TimeoutError, throw it
if (lastError.name !== 'TimeoutError')
throw lastError;
// define whether final attempt
const isFinalAttempt = attempt === maxRetries;
// define enriched error, attaching last known transaction hash
const enrichedError = isFinalAttempt && lastKnownTxHash && lastError.name === 'TimeoutError'
? new stargate_cjs_1.TimeoutError(`Transaction broadcast failed after ${maxRetries} attempts. Transaction hash: ${lastKnownTxHash}`, lastKnownTxHash)
: lastError;
// if final attempt and error does not have txHash, throw the last error
if (isFinalAttempt)
throw lastError;
// otherwise, brief backoff before retrying
await (0, utils_cjs_1.sleep)(1000);
}
}
// should not reach here
throw new stargate_cjs_1.TimeoutError(`Broadcast failed unexpectedly. Last known transaction hash: ${lastKnownTxHash ?? 'unknown'}`, lastKnownTxHash || 'unknown');
}
/**
* Broadcasts a signed transaction to the network without monitoring it.
*
* If broadcasting is rejected by the node for some reason (e.g. because of a CheckTx failure),
* an error is thrown.
*
* If the transaction is broadcasted, a `string` containing the hash of the transaction is returned. The caller then
* usually needs to check if the transaction was included in a block and was successful.
*
* @returns Returns the hash of the transaction
*/
async broadcastTxSync(tx) {
const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx });
if (broadcasted.code) {
return Promise.reject(new stargate_cjs_1.BroadcastTxError(broadcasted.code, broadcasted.codespace ?? '', broadcasted.log));
}
const transactionId = (0, encoding_cjs_1.toHex)(broadcasted.hash).toUpperCase();
return transactionId;
}
async simulate(signerAddress, messages, memo) {
if (!this.endpoint) {
throw new Error('querier: endpoint is not set');
}
const querier = await querier_1.CheqdQuerier.connect(this.endpoint);
const anyMsgs = messages.map((msg) => this.registry.encodeAsAny(msg));
const accountFromSigner = (await this._signer.getAccounts()).find((account) => account.address === signerAddress);
if (!accountFromSigner) {
throw new Error('Failed to retrieve account from signer');
}
const pubkey = (0, amino_cjs_1.encodeSecp256k1Pubkey)(accountFromSigner.pubkey);
const { sequence } = await this.getSequence(signerAddress);
const gasLimit = this.endpoint.includes('localhost')
? CheqdSigningStargateClient.maxGasLimit
: (await querier_1.CheqdQuerier.getConsensusParameters(this.endpoint)).block.maxGas;
const { gasInfo } = await (await this.constructSimulateExtension(querier)).tx.simulate(anyMsgs, memo, pubkey, signerAddress, sequence, gasLimit);
(0, utils_cjs_1.assertDefined)(gasInfo);
return math_cjs_2.Uint53.fromString(gasInfo.gasUsed.toString()).toNumber();
}
async constructSimulateExtension(querier) {
// setup rpc client
const rpc = (0, stargate_cjs_1.createProtobufRpcClient)(querier);
// setup query tx query service
const queryService = new service_1.ServiceClientImpl(rpc);
// setup + return tx extension
return {
tx: {
getTx: async (txId) => {
// construct request
const request = { hash: txId };
// query + return tx
return await queryService.GetTx(request);
},
simulate: async (messages, memo, signer, signerAddress, sequence, gasLimit) => {
// encode public key
const publicKey = (0, proto_signing_cjs_1.encodePubkey)(signer);
// construct max gas limit
const maxGasLimit = math_cjs_1.Int53.fromString(gasLimit.toString()).toNumber();
// construct unsigned tx
const tx = tx_js_1.Tx.fromPartial({
body: tx_js_1.TxBody.fromPartial({
messages: Array.from(messages),
memo,
}),
authInfo: tx_1.AuthInfo.fromPartial({
fee: tx_js_1.Fee.fromPartial({
amount: [],
gasLimit: maxGasLimit,
payer: signerAddress,
}),
signerInfos: [
{
publicKey,
modeInfo: {
single: { mode: signing_1.SignMode.SIGN_MODE_DIRECT },
},
sequence: sequence,
},
],
}),
signatures: [new Uint8Array()],
});
// construct request
const request = service_1.SimulateRequest.fromPartial({
txBytes: tx_js_1.Tx.encode(tx).finish(),
});
// query + return simulation response
return await queryService.Simulate(request);
},
},
};
}
async batchMessages(messages, signerAddress, memo, maxGasLimit = 30000000 // default gas limit, use consensus params if available
) {
// simulate
const gasEstimates = await Promise.all(messages.map(async (message) => this.simulate(signerAddress, [message], memo)));
// batch messages
const { batches, gasPerBatch, currentBatch, currentBatchGas } = gasEstimates.reduce((acc, gasUsed, index) => {
// finalise current batch, if limit is surpassed
if (acc.currentBatchGas + gasUsed > maxGasLimit) {
return {
batches: [...acc.batches, acc.currentBatch],
gasPerBatch: [...acc.gasPerBatch, acc.currentBatchGas],
currentBatch: [messages[index]],
currentBatchGas: gasUsed,
};
}
// otherwise, add to current batch
return {
batches: acc.batches,
gasPerBatch: acc.gasPerBatch,
currentBatch: [...acc.currentBatch, messages[index]],
currentBatchGas: acc.currentBatchGas + gasUsed,
};
}, {
batches: [],
gasPerBatch: [],
currentBatch: [],
currentBatchGas: 0,
});
// push final batch to batches, if not empty + return
return currentBatch.length > 0
? {
batches: [...batches, currentBatch],
gas: [...gasPerBatch, currentBatchGas],
}
: {
batches,
gas: gasPerBatch,
};
}
async checkDidSigners(verificationMethods = []) {
if (verificationMethods.length === 0) {
throw new Error('No verification methods provided');
}
verificationMethods.forEach((verificationMethod) => {
if (!Object.values(types_1.VerificationMethods).includes(verificationMethod.verificationMethodType ?? '')) {
throw new Error(`Unsupported verification method type: ${verificationMethod.verificationMethodType}`);
}
if (!this.didSigners[verificationMethod.verificationMethodType ?? '']) {
this.didSigners[verificationMethod.verificationMethodType ?? ''] = did_jwt_cjs_1.EdDSASigner;
}
});
return this.didSigners;
}
async getDidSigner(verificationMethodId, verificationMethods) {
await this.checkDidSigners(verificationMethods);
const verificationMethod = verificationMethods.find((method) => method.id === verificationMethodId)?.verificationMethodType;
if (!verificationMethod) {
throw new Error(`Verification method for ${verificationMethodId} not found`);
}
return this.didSigners[verificationMethod];
}
async signCreateDidDocTx(signInputs, payload) {
await this.checkDidSigners(payload?.verificationMethod);
const signBytes = v2_1.MsgCreateDidDocPayload.encode(payload).finish();
const signInfos = await Promise.all(signInputs.map(async (signInput) => {
return {
verificationMethodId: signInput.verificationMethodId,
signature: (0, did_jwt_cjs_1.base64ToBytes)((await (await this.getDidSigner(signInput.verificationMethodId, payload.verificationMethod))((0, did_jwt_cjs_1.hexToBytes)(signInput.privateKeyHex))(signBytes))),
};
}));
return signInfos;
}
async signUpdateDidDocTx(signInputs, payload, externalControllers, previousDidDocument) {
await this.checkDidSigners(payload?.verificationMethod);
const signBytes = v2_1.MsgUpdateDidDocPayload.encode(payload).finish();
const signInfos = await Promise.all(signInputs.map(async (signInput) => {
return {
verificationMethodId: signInput.verificationMethodId,
signature: (0, did_jwt_cjs_1.base64ToBytes)((await (await this.getDidSigner(signInput.verificationMethodId, payload.verificationMethod
.concat(externalControllers
?.flatMap((controller) => controller.verificationMethod)
.map((vm) => {
return {
id: vm.id,
verificationMethodType: vm.type,
controller: vm.controller,
verificationMaterial: '<ignored>',
};
}) ?? [])
.concat(previousDidDocument?.verificationMethod?.map((vm) => {
return {
id: vm.id,
verificationMethodType: vm.type,
controller: vm.controller,
verificationMaterial: '<ignored>',
};
}) ?? [])))((0, did_jwt_cjs_1.hexToBytes)(signInput.privateKeyHex))(signBytes))),
};
}));
return signInfos;
}
async signDeactivateDidDocTx(signInputs, payload, verificationMethod) {
await this.checkDidSigners(verificationMethod);
const signBytes = v2_1.MsgDeactivateDidDocPayload.encode(payload).finish();
const signInfos = await Promise.all(signInputs.map(async (signInput) => {
return {
verificationMethodId: signInput.verificationMethodId,
signature: (0, did_jwt_cjs_1.base64ToBytes)((await (await this.getDidSigner(signInput.verificationMethodId, verificationMethod))((0, did_jwt_cjs_1.hexToBytes)(signInput.privateKeyHex))(signBytes))),
};
}));
return signInfos;
}
static async signIdentityTx(signBytes, signInputs) {
let signInfos = [];
for (let signInput of signInputs) {
if (typeof signInput.keyType === undefined) {
throw new Error('Key type is not defined');
}
let signature;
switch (signInput.keyType) {
case 'Ed25519':
signature = (await (0, did_jwt_cjs_1.EdDSASigner)((0, did_jwt_cjs_1.hexToBytes)(signInput.privateKeyHex))(signBytes));
break;
case 'Secp256k1':
signature = (await (0, did_jwt_cjs_1.ES256KSigner)((0, did_jwt_cjs_1.hexToBytes)(signInput.privateKeyHex))(signBytes));
break;
case 'P256':
signature = (await (0, did_jwt_cjs_1.ES256Signer)((0, did_jwt_cjs_1.hexToBytes)(signInput.privateKeyHex))(signBytes));
break;
default:
throw new Error(`Unsupported signature type: ${signInput.keyType}`);
}
signInfos.push({
verificationMethodId: signInput.verificationMethodId,
signature: (0, did_jwt_cjs_1.base64ToBytes)(signature),
});
}
return signInfos;
}
}
exports.CheqdSigningStargateClient = CheqdSigningStargateClient;
//# sourceMappingURL=signer.js.map