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
648 stars 333 forks source link

Q: How to simulate MultisignedTx ? #1091

Open peng-huang-ch opened 2 years ago

peng-huang-ch commented 2 years ago

I'm trying to simulate the multi-signature to get the gas fee.I changed signerInfos to refer to the multisignature and simulate.

 const request = SimulateRequest.fromPartial({
    tx: Tx.fromPartial({
      authInfo: AuthInfo.fromPartial({
        fee: Fee.fromPartial({}),
        signerInfos: [
          {
            publicKey: multisigPubkey,
            sequence: long.fromNumber(signingInstruction.sequence, true),
            modeInfo: {
              multi: {
                bitarray: makeCompactBitArray([false, false, true, true, false]),
                modeInfos: [0, 1, 2, 3, 4].map((_) => ({ single: { mode: signing.SignMode.SIGN_MODE_LEGACY_AMINO_JSON } })),
              },
            },
          },
        ],
      }),
      body: TxBody.fromPartial({
        messages: signingInstruction.msgs,
        memo: signingInstruction.memo,
      }),
      signatures: [new Uint8Array()],
    }),
    txBytes: undefined,
  });
  console.log('msgs : ', signingInstruction.msgs);
  const response = await client.forceGetQueryClient().tx.request(request);
  console.log(' response : ', response);
response :  {
  key: Uint8Array(0) [],
  value: Uint8Array(0) [],
  proof: undefined,
  height: 365607,
  code: 18,
  index: 0,
  log: 'unable to resolve type URL : tx parse error: invalid request'
}
webmaster128 commented 2 years ago

Seems like messages: signingInstruction.msgs, is wrong. TxBody.messages is an array of Any, so the messages need to go through the Registry for encoding.

peng-huang-ch commented 2 years ago

Thanks for your help, here it's the signingInstruction.msgs. works fine on single address transactions

 [
    {
        "typeUrl": "/cosmos.bank.v1beta1.MsgSend",
        "value": {
            "fromAddress": "cosmos14txtjx9yx8zjxhu6s2jpa8xsx6auz3rhq7zhus", // multi address
            "toAddress": "cosmos19rvl6ja9h0erq9dc2xxfdzypc739ej8k5esnhg",
            "amount": [
                {
                    "amount": "1234",
                    "denom": "uphoton"
                }
            ]
        }
    }
]
webmaster128 commented 2 years ago

How do you construct multisigPubkey? Maybe this is where the type URL of the inner Any is an issue.

peng-huang-ch commented 2 years ago
const { createMultisigThresholdPubkey, pubkeyToAddress, encodeSecp256k1Pubkey } = require('@cosmjs/amino');
const { fromHex } = require('@cosmjs/encoding');
async function main() {
    const threshold = 2;
    const prefix = "cosmos";
    var pubkeys = [
        "02f4147da97162a214dbe25828ee4c4acc4dc721cd0c15b2761b43ed0292ed82b5",
        "0377155e520059d3b85c6afc5c617b7eb519afadd0360f1ef03aff3f7e3f5438dd",
        "02f44bce3eecd274e7aa24ec975388d12905dfc670a99b16e1d968e6ab5f69b266"
    ].map(key => encodeSecp256k1Pubkey(fromHex(key)));
    const multisigThresholdPubkey = createMultisigThresholdPubkey(pubkeys, threshold, true);
    const multisigAddress = pubkeyToAddress(multisigThresholdPubkey, prefix);
    console.log('multisigThresholdPubkey : ', JSON.stringify(multisigThresholdPubkey));
    console.log('multisigAddress : ', multisigAddress);

}
main().catch(console.error);

output:
multisigThresholdPubkey :  {"type":"tendermint/PubKeyMultisigThreshold","value":{"threshold":"2","pubkeys":[{"type":"tendermint/PubKeySecp256k1","value":"AvQUfalxYqIU2+JYKO5MSsxNxyHNDBWydhtD7QKS7YK1"},{"type":"tendermint/PubKeySecp256k1","value":"A3cVXlIAWdO4XGr8XGF7frUZr63QNg8e8Dr/P34/VDjd"},{"type":"tendermint/PubKeySecp256k1","value":"AvRLzj7s0nTnqiTsl1OI0SkF38ZwqZsW4dlo5qtfabJm"}]}}
multisigAddress :  cosmos1mca888pm39ld9zjnaagrjcjmtm27w0tzzaydct
peng-huang-ch commented 1 year ago

SDK Version: 0.29.5

Call the client.forceGetQueryClient().tx.simulate

Error: Query failed with (6): rpc error: code = Unknown desc = invalid from address: empty address string is not allowed: invalid address [/go/pkg/mod/cosmossdk.io/errors@v1.0.0-beta.7/errors.go:153] With gas wanted: '0' and gas used: '6663' : unknown request
    at QueryClient.queryAbci (~/workpalce/blockchain-notes/node_modules/.pnpm/@cosmjs+stargate@0.29.5/node_modules/@cosmjs/stargate/build/queryclient/queryclient.js:135:19)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.request (~/workpalce/blockchain-notes/node_modules/.pnpm/@cosmjs+stargate@0.29.5/node_modules/@cosmjs/stargate/build/queryclient/utils.js:35:30)
    at async Object.simulate (~/workpalce/blockchain-notes/node_modules/.pnpm/@cosmjs+stargate@0.29.5/node_modules/@cosmjs/stargate/build/modules/tx/queries.js:48:34)
    at async main (~/workpalce/blockchain-notes/cosmos/tx/multisig/xxx.js:137:28
require('dotenv').config();
const { strict: assert } = require('assert');
const { toHex } = require('@cosmjs/encoding');
const { coins, createMultisigThresholdPubkey, encodeSecp256k1Pubkey, pubkeyToAddress, makeCosmoshubPath } = require('@cosmjs/amino');
const { assertIsDeliverTxSuccess, SigningStargateClient, StargateClient, makeMultisignedTx } = require('@cosmjs/stargate');
const { Secp256k1HdWallet } = require('@cosmjs/amino');
const { TxRaw } = require("cosmjs-types/cosmos/tx/v1beta1/tx");

async function getMnemonicPubKeyAndAddress(mnemonic, prefix) {
  const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, {
    hdPaths: [makeCosmoshubPath(0)],
  });
  const [account] = await wallet.getAccounts();
  const secp256k1PubKey = encodeSecp256k1Pubkey(account.pubkey);
  const address = pubkeyToAddress(secp256k1PubKey, prefix);
  return { wallet, secp256k1PubKey, pubkey: toHex(account.pubkey), address };
}

async function signInstruction(mnemonic, instruction) {
  const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, {
    hdPaths: [makeCosmoshubPath(0)],
  });

  const [account] = await wallet.getAccounts();
  const address = account.address;
  const offlineSigningClient = await SigningStargateClient.offline(wallet);

  const pubkey = encodeSecp256k1Pubkey(account.pubkey);

  const signerData = {
    accountNumber: instruction.accountNumber,
    sequence: instruction.sequence,
    chainId: instruction.chainId,
  };

  const { bodyBytes: bb, signatures } = await offlineSigningClient.sign(
    address,
    instruction.msgs,
    instruction.fee,
    instruction.memo,
    signerData,
  );
  return [pubkey, signatures[0], bb];
}

// https://github.com/cosmos/cosmjs/blob/c192fc9b95ef97e4afbf7f5b94f8e8194ae428a6/packages/stargate/src/multisignature.spec.ts#L175
async function main() {
  const rpcEndpoint = '';
  const threshold = 2;
  const prefix = 'iaa';
  const denom = 'uiris';
  const alice = '';
  const bob = '';
  const carol = '';
  const mnemonics = [alice, bob, carol];
  const client = await StargateClient.connect(rpcEndpoint);

  const multisigAccountAddress = 'iaa18y4kun6wupgly9kja8awhnqpjhxt6hlj348923';
  const receipt = 'iaa18y4kun6wupgly9kja8awhnqpjhxt6hlj348923';

  const keys = await Promise.all(mnemonics.map((mnemonic) => getMnemonicPubKeyAndAddress(mnemonic, prefix)));

  const secp256k1PubKeys = keys.map((item) => item.secp256k1PubKey);

  var multisigPubkey = createMultisigThresholdPubkey(secp256k1PubKeys, threshold, true);
  const multisigAddress = pubkeyToAddress(multisigPubkey, prefix);
  console.log('multisigAddress : ', multisigAddress);

  // account
  const accountOnChain = await client.getAccount(multisigAddress);
  assert(accountOnChain, 'Account does not exist on chain');
  console.log('accountOnChain : ', accountOnChain);

  // balance
  const balance = await client.getBalance(multisigAddress, 'uiris');
  console.log('balance', balance);

  const msgSend = {
    fromAddress: multisigAddress,
    toAddress: receipt,
    amount: coins(2000, denom),
  };
  const msg = {
    typeUrl: '/cosmos.bank.v1beta1.MsgSend',
    value: msgSend,
  };

  const gasLimit = 200000;
  const fee = {
    amount: coins(2000, denom),
    gas: gasLimit.toString(),
  };

  const chainId = await client.getChainId();
  const memo = 'multisig memo';

  // On the composer's machine signing instructions are created.
  const signingInstruction = {
    accountNumber: accountOnChain.accountNumber,
    sequence: accountOnChain.sequence,
    chainId,
    msgs: [msg],
    fee,
    memo,
  };

  const [
    [pubkey0, signature0, bodyBytes], //
    [pubkey1, signature1], //
    [pubkey2, signature2] //
  ] = await Promise.all(
    [AARON, PHCC, PENG].map(async (mnemonic) => signInstruction(mnemonic, signingInstruction, rpcEndpoint)));
  const address0 = pubkeyToAddress(pubkey0, prefix);
  const address1 = pubkeyToAddress(pubkey1, prefix);
  const address2 = pubkeyToAddress(pubkey2, prefix);

  var multisigPubkey = createMultisigThresholdPubkey([pubkey0, pubkey1, pubkey2], threshold, true);
  assert.strictEqual(multisigAccountAddress, pubkeyToAddress(multisigPubkey, prefix), 'should be equal');

  const signedTx = makeMultisignedTx(
    multisigPubkey,
    signingInstruction.sequence,
    signingInstruction.fee,
    bodyBytes,
    new Map([
      [address0, signature0],
      [address1, signature1],
      // [address2, signature2],
    ])
  );

  // const queryClient = client.forceGetQueryClient();
  // const simulateResponse = await queryClient.tx.simulate(signingInstruction.msgs, signingInstruction.memo, multisigPubkey, signingInstruction.sequence);
  // console.log("simulateResponse : ", simulateResponse);
  // return;
  // Error: Query failed with (6): rpc error: code = Unknown desc = invalid from address: empty address string is not allowed: invalid address [/go/pkg/mod/cosmossdk.io/errors@v1.0.0-beta.7/errors.go:153] With gas wanted: '0' and gas used: '6663' : unknown request
  const tx = TxRaw.encode(signedTx).finish();
  const result = await client.broadcastTx(tx);
  assertIsDeliverTxSuccess(result);
  const { transactionHash } = result;
  console.log('tx : ', `https://www.mintscan.io/iris/txs/${transactionHash}`);
}

main().catch(console.error);