cosmos / cosmjs

The Swiss Army knife to power JavaScript based client solutions ranging from Web apps/explorers over browser extensions to server-side clients like faucets/scrapers.
https://cosmos.github.io/cosmjs/
Apache License 2.0
646 stars 331 forks source link

Sending out transaction fails with sdk code 2, and shows no typeURL #1420

Closed codemaster101 closed 1 year ago

codemaster101 commented 1 year ago

Hi, I am trying to send out a transaction on Cosmos testnet. When I use client.sendTokens it works perfectly fine, but when I try to recreate the sendTokens function (because we need signBytes, as we sign outside the SDK on an HSM to get back digital signature), it fails. Currently I am using the soft-signer (which Direct Sign uses in its implementation).

The following is my implementation to send out tokens:

import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx.js"
import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx.js";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin.js";
import {
    makeSignBytes,
    makeSignDoc,
    makeAuthInfoBytes,
    coins,
    Registry,
    DirectSecp256k1Wallet,
} from "@cosmjs/proto-signing";
import { fromBase64 } from "@cosmjs/encoding";
import { SigningStargateClient } from "@cosmjs/stargate";
import { Secp256k1, sha256 } from "@cosmjs/crypto";
import { encodeSecp256k1Signature } from "@cosmjs/amino";

import _m0 from 'protobufjs/minimal.js';

import { GasPrice } from "./fee.js"; // Gas Price Implementation from fee.js in the Cosmos SDK
import { Account } from './account.js'; // Connect to node and get back account information
import Wallet from './createWallet.js'; // Used to get back private key
import { DENOM_COSMOS } from './constants.js'; // uatom

const defaultSigningClientOptions = {
    broadcastPollIntervalMs: 300,
    broadcastTimeoutMs: 8_000,
    gasPrice: GasPrice.fromString("0.00001uatom"),
  };

const createMsgSendObject = () => {
    // Amount object
    const amount = {
        denom: DENOM_COSMOS,
        amount: '100',
    };

    const msgSendValue = MsgSend.fromPartial({
        fromAddress: 'cosmos1vuqn5p2jm5vf3n9595t44dydxhtjvygnnspf8y',
        toAddress: 'cosmos1vuqn5p2jm5vf3n9595t44dydxhtjvygnnspf8y',
        amount: [amount],
    });

    // MsgSend Object
    const msgSendData = {
        typeUrl: '/cosmos.bank.v1beta1.MsgSend',
        value: msgSendValue,
    }

    // Memo Field
    const memo = 'First tx on Cosmos';

    return { msgSendData, amount, memo };
}

const sendFuncCosmos = async () => {
    const privKey = await Wallet.createHdWallet();
    const wallet = await DirectSecp256k1Wallet.fromKey(privKey.privateKey);

    const client = await SigningStargateClient.connectWithSigner(
        'https://rpc.sentry-01.theta-testnet.polypore.xyz',
        wallet,
        defaultSigningClientOptions,
    );

    const { msgSendData, amount, memo } = createMsgSendObject();

    const gasUsed = (await client.simulate(
        'cosmos1vuqn5p2jm5vf3n9595t44dydxhtjvygnnspf8y',
        [msgSendData],
        memo,
    ));

    console.log({ gasUsed }); // Works
    console.log({ gasUsedAugment: gasUsed+80000})

    // Generating fee object
    const gasPrice = defaultSigningClientOptions.gasPrice;
    const { denom, amount: gasPriceAmount } = gasPrice;
    const gasPriceInLowest = gasPriceAmount * (10**6);

    console.log('---------------------------------------------')
    console.log({ gasPriceInLowest, gasUsed });
    const gpa = gasPriceInLowest * gasUsed;

    const fee = {
        amount: coins(gpa, 'uatom'),
        gas: (200000).toString(),// gasUsed.toString(),
    };

    console.log(fee);

    // ! Working piece of code.
    // const result2 = await client.sendTokens(
    //     'cosmos1vuqn5p2jm5vf3n9595t44dydxhtjvygnnspf8y',
    //     'cosmos1vuqn5p2jm5vf3n9595t44dydxhtjvygnnspf8y',
    //     [amount],
    //     fee,
    //     memo,
    // );

    // console.log(result2);

    // Formulating what happens in sendTokens

    // Exact TxBody
    // TxBody Object - TypeURL + Value (Msg + Memo)
    console.log({ msgSendData });
    const txBodyEncodeObject = {
        typeUrl: "/cosmos.tx.v1beta1.TxBody",
        value: {
            messages: [msgSendData],
            memo,
        },
    };

    // Creating a default registry, this should be more or less near as to what is needed
    // If not then this part needs to be reformatted
    const defaultRegistryTypes = [
        ["/cosmos.bank.v1beta1.MsgSend", MsgSend],
        ["/cosmos.base.v1beta1.Coin", Coin],
    ];
    function createDefaultRegistry() {
        return new Registry(defaultRegistryTypes);
    }
    const registry = createDefaultRegistry();

    console.log('------------------------------------------------------------');
    console.log(registry);
    console.log('------------------------------------------------------------');

    console.log({ txBodyEncodeObject });
    const bodyBytes = registry.encode(txBodyEncodeObject);
    console.log({ bodyBytes });

    // const pubKey: Any = 'AwJFg6IdSdtJkCH7epziQ46JtREGCW2E2iA8E08t00M4';

    // Get back account and sequence number
    const acc = await Account.GetInstance('cosmos');
    const { accountNumber, sequenceNumber } = await acc.getAccount('cosmos1vuqn5p2jm5vf3n9595t44dydxhtjvygnnspf8y');

    const authInfoBytes = makeAuthInfoBytes(
        [{
            pubkey: wallet,
            sequence: sequenceNumber,
        }],
        fee.amount /* Total fee amount */,
        200000 /* Gas Limit */,
        undefined,
        undefined,
    );

    console.log({ bodyBytes, authInfoBytes });

    // Creating a valid signature
    const signDoc = makeSignDoc(
        bodyBytes,
        authInfoBytes,
        'theta-testnet-001',
        accountNumber,
    );

    const signBytesFromSignDoc = makeSignBytes(signDoc);
    const hashedMessage = sha256(signBytesFromSignDoc);
    const signature = await Secp256k1.createSignature(
        hashedMessage,
        privKey.privateKey,
    );

    const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]);
    const standardSignature = encodeSecp256k1Signature(
        privKey.publicKey,
        signatureBytes,
    );

    console.log({ signBytesFromSignDoc, signDoc });

    const txRaw = TxRaw.fromPartial({
        bodyBytes,
        authInfoBytes,
        signatures: [fromBase64(standardSignature.signature)],
    });

    const signedTransactionBytes = TxRaw.encode(txRaw).finish();
    console.log({ signedTransactionBytes });

    const result = await client.broadcastTx(signedTransactionBytes);
    console.log(result);
}

sendFuncCosmos();

This yields the following error:

<redacted>/tests/cosmos/node_modules/@cosmjs/stargate/build/stargateclient.js:283
            return Promise.reject(new BroadcastTxError(broadcasted.code, broadcasted.codespace ?? "", broadcasted.log));
                                  ^

BroadcastTxError: Broadcasting transaction failed with code 2 (codespace: sdk). Log: unable to resolve type URL : tx parse error
    at SigningStargateClient.broadcastTx (<redacted>/tests/cosmos/node_modules/@cosmjs/stargate/build/stargateclient.js:283:35)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async sendFuncCosmos (<redacted>/tests/cosmos/test.js:222:20) {
  code: 2,
  codespace: 'sdk',
  log: 'unable to resolve type URL : tx parse error'
}

The signed transaction in hexadecimal is the following: '0aa3010a8c010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126c0a2d636f736d6f73317675716e3570326a6d357666336e393539357434346479647868746a7679676e6e7370663879122d636f736d6f73317675716e3570326a6d357666336e393539357434346479647868746a7679676e6e73706638791a0c0a057561746f6d120331303012124669727374207478206f6e20436f736d6f7312210a080a0012040a02080112150a0f0a057561746f6d120636303637343010c09a0c1a4038625c4c06c4e204c4645a0896abe9f55e4a845111291d2b5d8330896c7af624746ab2c11adf30464169c52c73ce2a1eb43aea95821261468b741bd4e15af7a7'

Any help in this regard would be greatly appreciated. Am I doing something wrong or there is a missing field somewhere?

codemaster101 commented 1 year ago

Hmm, got it.

    const authInfoBytes = makeAuthInfoBytes(
        [{
            pubkey: wallet,
            sequence: sequenceNumber,
        }],
        fee.amount /* Total fee amount */,
        200000 /* Gas Limit */,
        undefined,
        undefined,
    );

I am supposed to provide a encodePubkey object here instead of wallet. I tried submitting the encodeSecp256k1Pubkey object here, but it was failing. So from a secp256k1 key => encode into object for secp256k1 tendermint key => encodePubkey with TypeUrl