Ethereum Transaction

Examples showing how to do Ethereum transactions with the TSM

To carry out an Ethereum transaction using the TSM, ensure you've installed the necessary Ethereum NPM dependencies. Here's an example of Ethereum NPM dependencies:

{
  "dependencies": {
    "@ethereumjs/common": "^4.0.0",
    "@sepior/tsm": "^55.2.1",
    "asn1.js": "^5.4.1",
    "ethereum-tx-decoder": "^3.0.0",
    "ethers": "^5.7.2",
    "keccak256": "^1.0.6",
    "web3": "^4.1.2"
  }
}

After successfully installing the Ethereum NPM dependencies, you can now proceed with an Ethereum transaction using TSM. Here's an example of how to execute an Ethereum transaction with TSM:

const { Web3 } = require("web3");
const { Transaction } = require("web3-eth-accounts");

const ganacheUrl = 'http://127.0.0.1:7545';
const httpProvider = new Web3.providers.HttpProvider(ganacheUrl);
// const httpProvider = new Web3.providers.HttpProvider("https://eth-sepolia.g.alchemy.com/v2/your-api-token");
const web3 = new Web3(httpProvider);

const fs = require("fs");
const keccak256 = require("keccak256");
const { TSMClient, algorithms, curves } = require("@sepior/tsm");
const credsRaw = fs.readFileSync("creds.json");
const creds = JSON.parse(credsRaw);
const asn = require("asn1.js");
const ethers = require("ethers");
const { Chain, Common, Hardfork } = require("@ethereumjs/common");
const chainId = Chain.Mainnet; // The chain ID of your local network, use Chain.Sepolia for sepolia, Chain.Mainnet for Ganache  
const txDecoder = require('ethereum-tx-decoder');

async function sendTxn() {
  const common = new Common({ chain: chainId, hardfork: Hardfork.Cancun})

  let playerCount = 3;
  let threshold = 1;

  let tsmClient1 = await TSMClient.init(playerCount, threshold, creds.creds1);
  let tsmClient2 = await TSMClient.init(playerCount, threshold, creds.creds2);
  let tsmClient3 = await TSMClient.init(playerCount, threshold, creds.creds3);

  let sessionNum = generateRandomNumber(43);
  sessionID = "e" + sessionNum;

  // If you need to generate a new key, uncomment the following snippet:

  // let results = await Promise.all([
  //   tsmClient1.keygenWithSessionID(
  //     algorithms.ECDSA,
  //     sessionID,
  //     curves.SECP256K1
  //   ),
  //   tsmClient2.keygenWithSessionID(
  //     algorithms.ECDSA,
  //     sessionID,
  //     curves.SECP256K1
  //   ),
  //   tsmClient3.keygenWithSessionID(
  //     algorithms.ECDSA,
  //     sessionID,
  //     curves.SECP256K1
  //   ),
  // ]);
  // console.log("Results from keygenWithSessionID:", results);
  // keyID = results[0]; // KeyIDs from different players should be validated to be equal
  const keyID = "JgHDgME3J28Iu3cfMJ5JTSxkCuWd"; 
  console.log("Using key with key ID:", keyID);

  sessionNum = generateRandomNumber(44);
  sessionID = "e" + sessionNum;

  // Generate one presignature for use in signing. In production many presignatures should be created once, and then used afterwards.
  presigCount = 1;
  results = await Promise.all([
    tsmClient1.presigGenWithSessionID(
      algorithms.ECDSA,
      sessionID,
      keyID,
      presigCount
    ),
    tsmClient2.presigGenWithSessionID(
      algorithms.ECDSA,
      sessionID,
      keyID,
      presigCount
    ),
    tsmClient3.presigGenWithSessionID(
      algorithms.ECDSA,
      sessionID,
      keyID,
      presigCount
    ),
  ]);
  presigIDs = results[0]; // PresigIDs from different players should be validated to be equal
  console.log("Generated presigs with IDs:", presigIDs);
  let chainPath = new Uint32Array([0, 3]);

  let [, derPk] = await tsmClient1.publicKey(
    algorithms.ECDSA,
    keyID,
    chainPath
  );
  // Parse ECDSA Public Key to derive address
  let hexEncodedXY = derPk.subarray(-64);
  let decodedPkValue = Buffer.from(hexEncodedXY);
  const rawPublicKey = decodedPkValue.toString("hex");
  console.log("Raw public key: 0x" + rawPublicKey);

  var hashValueKeccak = keccak256(decodedPkValue);
  console.log("hashValueKeccak", hashValueKeccak.toString("hex"));
  var senderAddress = Buffer.from(hashValueKeccak.subarray(-20)).toString("hex");
  console.log("Sender address: ", senderAddress);

  // Get Balance of the newly created account
  senderAddress = "0x" + senderAddress;
  let balance = await getEthBalance(senderAddress);
  console.log("Initial funding - Eth account balance before transfer:", balance);
  const accounts = await web3.eth.getAccounts();

  // Fund newly created account
  const txn = await web3.eth.sendTransaction({
    to: senderAddress,
    from: accounts[4],
    value: web3.utils.toWei("1", "ether"),
  });

  balance = await getEthBalance(senderAddress);
  console.log("Initial funding - Eth account balance after transfer:", balance);

  // Create a raw Ethereum transaction
  const addressTo = accounts[3]; // Some hex encoded receiver address like "0x5Ff40197C83C3A2705ba912333Cf1a37BA249eB7"
  let count = await web3.eth.getTransactionCount(senderAddress, "latest");
  const transferAmountValue = web3.utils.numberToHex(
    web3.utils.toWei("0.1", "ether")
  );
  // Gas price and limit can be retrieved from the provider
  // const gasPrice = await web3.eth.getGasPrice();
  // const gasLimit = (await web3.eth.getBlock("latest")).gasLimit;

  let rawTxn = {
    nonce: web3.utils.numberToHex(count),
    // gasPrice: web3.utils.toHex(gasPrice),
    // gasLimit: web3.utils.toHex(gasLimit),
    gasPrice: web3.utils.numberToHex(web3.utils.toWei("2200", "Gwei")),
    gasLimit: web3.utils.numberToHex(200000),
    to: addressTo,
    from: senderAddress,
    value: transferAmountValue,
    data: "0x",
    chainId: chainId,
  };


  const transaction = Transaction.fromTxData(rawTxn, {common: common, freeze: false});
  if (chainId != Chain.Sepolia) {
    transaction.activeCapabilities = []; // Remove eip155 for Ganache
  }

  let unsignedTxHash = transaction.getMessageToSign(true);

  // generate partial signature
  presigID = presigIDs[0]; // Or use "" for an arbitrary presignature 

  let [partialSignature1] = await tsmClient1.partialSignWithPresig(
    algorithms.ECDSA,
    keyID,
    presigID,
    chainPath,
    unsignedTxHash
  );
  let [partialSignature2] = await tsmClient2.partialSignWithPresig(
    algorithms.ECDSA,
    keyID,
    presigID,
    chainPath,
    unsignedTxHash
  );
  let [partialSignature3] = await tsmClient3.partialSignWithPresig(
    algorithms.ECDSA,
    keyID,
    presigID,
    chainPath,
    unsignedTxHash
  );

  let [aggregatedSignatureDER, recoveryID] = await tsmClient1.finalize(
    algorithms.ECDSA,
    [partialSignature1, partialSignature2, partialSignature3]
  );
  console.log("aggregatedSignatureDER :", aggregatedSignatureDER);
  console.log("recoveryID :", recoveryID);

  let isValidDERSig = await tsmClient1.verify(
    algorithms.ECDSA,
    derPk,
    unsignedTxHash,
    aggregatedSignatureDER,
    curves.SECP256K1
  );
  console.log("Is DER signature valid?", isValidDERSig);

  const { ecSignature, r, s, signature } = ASN1ParseSecp256k1Signature(
    aggregatedSignatureDER
  );

  const sig = {
    r: "0x" + r,
    s: "0x" + s,
    v: chainId == Chain.Sepolia ? web3.utils.numberToHex(recoveryID + chainId * 2 + 35) : web3.utils.numberToHex(recoveryID + 27)
  };

  transaction.r = web3.utils.toBigInt(sig.r);
  transaction.s = web3.utils.toBigInt(sig.s);
  transaction.v = web3.utils.toBigInt(sig.v);

  let signedTxn = transaction.serialize();
  signedTxn = web3.utils.bytesToHex(signedTxn);

  console.log("decoded txn", txDecoder.decodeTx(signedTxn))

  const recoveredAddress = await ethers.utils.recoverAddress(unsignedTxHash, sig);
  console.log("recoveredAddress equals sender address: ", recoveredAddress.toLowerCase() === senderAddress.toLowerCase(), recoveredAddress, senderAddress);

    
  const hash = await web3.eth.sendSignedTransaction(signedTxn)
      .on('error', error => console.log("error: ", error))
      .on('confirmation', confirmation => console.log("confirmation: ", confirmation))
      .on('receipt', receipt => console.log("receipt: ", receipt))
      .on('sent', sentTransaction => console.log("sentTransaction: ", sentTransaction))
      .on('sending', transactionToBeSent => console.log("transactionToBeSent: ", transactionToBeSent))
      .catch(console.log);

    console.log("hash: ", hash);
}

sendTxn();

function ASN1ParseSecp256k1Signature(derSignature) {
  // Parse aggregated signature
  var ECSignature = asn.define('ECSignature', function () {
    this.seq().obj(
        this.key('R').int(),
        this.key('S').int()
    );
  });

  var ecSignature = ECSignature.decode(Buffer.from(derSignature), "der");
  console.log("ecSignature", ecSignature);
  let signature =
    ecSignature.R.toString("hex").padStart(64, "0") +
    ecSignature.S.toString("hex").padStart(64, "0");
  signature = Buffer.from(signature, "hex");
  return {
    ecSignature,
    r: ecSignature.R.toString("hex"),
    s: ecSignature.S.toString("hex"),
    signature,
  };
}

function generateRandomNumber(n) {
  var add = 1,
    max = 12 - add; // 12 is the min safe number Math.random() can generate without it starting to pad the end with zeros.
  if (n > max) {
    return generateRandomNumber(max) + generateRandomNumber(n - max);
  }
  max = Math.pow(10, n + add);
  var min = max / 10; // Math.pow(10, n) basically
  var number = Math.floor(Math.random() * (max - min + 1)) + min;
  return ("" + number).substring(add);
}

async function getEthBalance(address) {
  let balance = await web3.eth.getBalance(address);
  balance = web3.utils.fromWei(balance, "ether");
  return balance;
}