NEXUS

Sign Fee Delegation Transaction

Using the go-cross Package

Below is a brief introduction to the go-cross code snippet demonstrating how to implement a Fee Delegated Dynamic Fee transaction in a custom Go environment. Rather than relying on external libraries, this snippet extends geth-style logic to accommodate advanced transaction types, including fee delegation. Written in Go (e.g., Go 1.18+), it illustrates how both the sender and fee payer signatures can be managed within a single transaction type.

After this overview, the code itself follows, accompanied by detailed, line-by-line commentary. You will see how the core DynamicFeeTx structure is extended to create a FeeDelegatedDynamicFeeTx that captures fee payer information and signatures separately. This guide provides an example of how to structure a robust, independent fee delegation mechanism.

package feedelegation

import (
	"crypto/ecdsa"
	"fmt"

	"github.com/nexus/go-cross/core/types"
	"github.com/nexus/go-cross/crypto"
)


// SignFeeDelegationTx function takes an existing Dynamic Fee transaction (senderTx)
// and adds the 'fee payer' signature (ecdsaPayer) to create a Fee Delegation transaction.
// It returns the newly created transaction and an error if any occurs.
func SignFeeDelegationTx(senderTx *types.Transaction, ecdsaPayer *ecdsa.PrivateKey) (feepayerTx *types.Transaction, err error) {

	// 1) First, check if the given transaction (senderTx) is a Dynamic Fee transaction (i.e., if its type is DynamicFeeTxType).
	// If it's not a DynamicFeeTx, we cannot convert it for Fee Delegation, so we return an error.
	if senderTx.Type() != types.DynamicFeeTxType {
		return nil, fmt.Errorf("transaction type not supported")
	}

	// 2) Since we've confirmed that senderTx is a Dynamic Fee transaction,
	//    we extract its internal data (transaction fields) as a DynamicFeeTx structure.
	senderTxData := senderTx.GetTxData().(*types.DynamicFeeTx)

	// 3) From the fee payer's private key (ecdsaPayer), we obtain the public key,
	//    and then calculate the fee payer's address from that public key.
	payer := crypto.PubkeyToAddress(ecdsaPayer.PublicKey)

	// 4) Using the extracted senderTxData (the original DynamicFeeTx information),
	//    create a FeeDelegatedDynamicFeeTx structure.
	//    This step effectively creates a "Fee Delegation enabled" version of the DynamicFee transaction.
	//
	//    NewFeeDelegatedDynamicFeeTx signature:
	//      func NewFeeDelegatedDynamicFeeTx(feePayer *common.Address, txData DynamicFeeTx) *FeeDelegatedDynamicFeeTx
	//
	//    First parameter: feePayer (the fee payer’s address)
	//    Second parameter: the original transaction data (DynamicFeeTx structure)
	feepayerTxData := types.NewFeeDelegatedDynamicFeeTx(&payer, *senderTxData)

	// 5) Based on the newly created FeeDelegatedDynamicFeeTx data, we generate a new transaction object.
	//    types.NewTx() initializes structures needed for RLP encoding and so forth.
	feepayerTx = types.NewTx(feepayerTxData)

	// 6) Create a FeeDelegationSigner using the Chain ID.
	//    This Signer will let the fee payer (ecdsaPayer) sign the FeeDelegatedDynamicFeeTx.
	signer := types.NewFeeDelegationSigner(senderTxData.ChainID)

	// 7) Now we sign the newly created FeeDelegation transaction (feepayerTx) with the fee payer’s private key.
	//    types.SignTx() handles adding the necessary signing fields for RLP encoding.
	feepayerTx, err = types.SignTx(feepayerTx, signer, ecdsaPayer)

	// 8) Finally, return the signed FeeDelegation transaction (feepayerTx) and any error that occurred.
	return
}

Using Node.js

Below is a brief introduction to the Node.js snippet, which demonstrates how to implement a Fee Delegated Dynamic Fee transaction in a custom environment. This example uses a combination of Node.js and ethers.js—along with a custom transaction type and signature logic—to show how both the sender and a separate fee payer can participate in a single transaction flow. After this introduction, you will find the annotated code itself, detailing how to structure and serialize a Fee Delegated Dynamic Fee transaction entirely in Node.js, independent of any other specialized libraries.

/***********************************************
 * Fee Delegation Transaction Example (Node.js)
 * ---------------------------------------------
 * - Uses ethers.js
 * - Demonstrates a custom "FeeDelegatedDynamicFeeTx" type
 * - Mimics the logic from your Go-based feeDelegationSigner
 ************************************************/

const { ethers } = require("ethers");
const RLP = require("rlp");          // npm install rlp
const keccak256 = require("keccak256"); // npm install keccak256
const BigNumber = ethers.BigNumber;  // For handling large integers (chainId, etc.)

/**
 * A simple class to hold the fields of a Fee Delegated Dynamic Fee Transaction
 * (somewhat emulating Klaytn's specialized type).
 *
 * This is NOT a standard Ethereum/EIP-1559 transaction. It's purely a sample
 * showing how you might approach custom transaction handling. In real usage,
 * you'd adapt it to match Klaytn's RLP specs & network behavior.
 */
class FeeDelegatedDynamicFeeTx {
  constructor({
    chainId,
    nonce,
    gasTipCap,
    gasFeeCap,
    gasLimit,
    to,
    value,
    data,
    accessList,
    // Signatures from the sender
    senderV,
    senderR,
    senderS,
    // Fee payer info
    feePayer,
    // Fee payer's signature
    feePayerV,
    feePayerR,
    feePayerS,
  }) {
    // Basic EIP-1559 / dynamic fee fields
    this.chainId = BigNumber.from(chainId || 0);
    this.nonce = BigNumber.from(nonce || 0);
    this.gasTipCap = BigNumber.from(gasTipCap || 0);
    this.gasFeeCap = BigNumber.from(gasFeeCap || 0);
    this.gasLimit = BigNumber.from(gasLimit || 0);
    this.to = to || null;      // Can be null (contract creation)
    this.value = BigNumber.from(value || 0);
    this.data = data || "0x";
    this.accessList = accessList || [];

    // Sender signature fields
    this.senderV = senderV || null;
    this.senderR = senderR || null;
    this.senderS = senderS || null;

    // Fee payer info
    this.feePayer = feePayer || null;
    this.feePayerV = feePayerV || null;
    this.feePayerR = feePayerR || null;
    this.feePayerS = feePayerS || null;
  }

  /**
   * Return the “type” that this transaction is meant to represent.
   * In standard Ethereum, EIP-1559 is type 2 (0x2),
   * but here we'll just create a custom type code for demonstration.
   */
  getType() {
    // Use a custom numeric code, for example 0x30 or 0x09, etc.
    // The actual code for "Fee Delegated" in Klaytn is different,
    // but let's assume 0x09 for demonstration.
    return 0x09;
  }

  /**
   * Check if the transaction has a valid “sender” EIP-1559 signature.
   * (This is just a placeholder – for a real system, you'd do more.)
   */
  isSignedBySender() {
    return this.senderR && this.senderS && this.senderV;
  }

  /**
   * Serialize the transaction fields for hashing:
   * This tries to mimic Klaytn’s approach of:
   *   RLP([ chainId, nonce, gasTipCap, gasFeeCap, gas, to, value, data, accessList, v, r, s ])
   * plus the feePayer. Then we do “prefixedRlpHash” including feePayer address.
   *
   * This is just one possible approach and not an official Klaytn format.
   */
  rlpForHashing() {
    // Convert each field to hex or buffer as needed for RLP
    const chainIdHex = ethers.utils.hexValue(this.chainId);
    const nonceHex = ethers.utils.hexValue(this.nonce);
    const gasTipCapHex = ethers.utils.hexValue(this.gasTipCap);
    const gasFeeCapHex = ethers.utils.hexValue(this.gasFeeCap);
    const gasLimitHex = ethers.utils.hexValue(this.gasLimit);
    const toHex = this.to ? this.to : "0x";
    const valueHex = ethers.utils.hexValue(this.value);

    // Sender signature placeholders, in Klaytn you'd store them and RLP them
    const vHex = this.senderV ? ethers.utils.hexValue(this.senderV) : "0x";
    const rHex = this.senderR ? ethers.utils.hexValue(this.senderR) : "0x";
    const sHex = this.senderS ? ethers.utils.hexValue(this.senderS) : "0x";

    // RLP for EIP-1559-like fields plus the original sender’s signature
    const items = [
      this.chainId.toHexString(),
      nonceHex,
      gasTipCapHex,
      gasFeeCapHex,
      gasLimitHex,
      toHex,
      valueHex,
      this.data,
      this.accessList.map((entry) => {
        // real AccessList is more complex; for example: [address, [storageKeys...]]
        return [entry.address, entry.storageKeys || []];
      }),
      // Original sender signature
      vHex,
      rHex,
      sHex,
    ];

    const encoded = RLP.encode(items);

    // According to the Go code, there's a portion adding the feePayer in the hash:
    // "..., feePayer()" as the final portion. We do that for demonstration:
    const withFeePayer = RLP.encode([encoded, this.feePayer || "0x"]);
    return withFeePayer;
  }

  /**
   * Calculate the transaction hash that the fee payer will sign.
   */
  hashForFeePayerSignature() {
    const typed = [
      this.getType(),
      this.rlpForHashing(), // The RLP-encoded data
    ];

    const encoded = RLP.encode(typed);
    return ethers.utils.hexlify(keccak256(encoded));
  }

  /**
   * Attach the fee payer’s signature to this transaction (similar to signAsFeePayer).
   */
  attachFeePayerSignature(sig) {
    // sig is a 65-byte ECDSA signature: [R(32), S(32), recovery(1)]
    const r = ethers.utils.hexDataSlice(sig, 0, 32);
    const s = ethers.utils.hexDataSlice(sig, 32, 64);
    const v = ethers.utils.hexDataSlice(sig, 64, 65); // This is the recovery id
    // Typically you'd do v + 27, check chainId, etc. For demonstration:
    const vBN = BigNumber.from(v).add(27);

    this.feePayerR = r;
    this.feePayerS = s;
    this.feePayerV = vBN;
  }

  /**
   * Serialize the entire transaction (including the fee payer signature)
   * into a raw RLP that you might send as a "raw transaction" on Klaytn.
   *
   * Real Klaytn’s "Fee Delegated" encoding is more nuanced. This is a conceptual example.
   */
  serialize() {
    // Now that the fee payer signature is attached, we create the final RLP
    // with all fields (sender signature + fee payer signature).
    //
    // For demonstration, let's embed the fee payer signature in an extra array item.
    // The final shape might differ from real Klaytn.

    const rFeePayerHex = this.feePayerR ? ethers.utils.hexValue(this.feePayerR) : "0x";
    const sFeePayerHex = this.feePayerS ? ethers.utils.hexValue(this.feePayerS) : "0x";
    const vFeePayerHex = this.feePayerV ? ethers.utils.hexValue(this.feePayerV) : "0x";

    const baseRlp = this.rlpForHashing(); // This includes up to sender’s sig & feePayer address

    // We'll do: RLP([ type, baseRlp, feePayerV, feePayerR, feePayerS ])
    const finalItems = [
      this.getType(),              // custom type
      baseRlp,                     // original fields
      vFeePayerHex,                // fee payer's V
      rFeePayerHex,                // fee payer's R
      sFeePayerHex,                // fee payer's S
    ];

    return ethers.utils.hexConcat(["0x", RLP.encode(finalItems)]);
  }
}

/**
 * signFeeDelegationTx:
 * Emulates the logic from the Go function `SignFeeDelegationTx`. 
 * 
 * 1) Verifies the type is our "Fee Delegated Dynamic Fee Tx".
 * 2) Uses a fee payer private key to sign the transaction’s fee delegation fields.
 * 3) Returns a new transaction object (with fee payer signature attached).
 */
async function signFeeDelegationTx(senderTx, feePayerPrivateKey) {
  // 1) Check if the transaction is the correct type.
  if (senderTx.getType() !== 0x09) {
    throw new Error("transaction type not supported for fee delegation");
  }

  // 2) If we haven't set a feePayer address yet, derive it from the private key:
  if (!senderTx.feePayer) {
    const wallet = new ethers.Wallet(feePayerPrivateKey);
    senderTx.feePayer = wallet.address;
  }

  // 3) Generate the hash that the fee payer will sign:
  const msgHash = senderTx.hashForFeePayerSignature();

  // 4) Sign with fee payer's private key using ethers Wallet
  const feePayerWallet = new ethers.Wallet(feePayerPrivateKey);

  // Ethers doesn't sign raw 32-byte messages by default with no prefix, 
  // so we'll use "signingKey" directly:
  const signature = feePayerWallet._signingKey().signDigest(msgHash);

  // 5) Convert the signature to a 65-byte hex string
  const sigBytes = ethers.utils.joinSignature(signature);

  // 6) Attach the fee payer signature to the transaction
  senderTx.attachFeePayerSignature(sigBytes);

  // 7) Return the updated transaction object
  return senderTx;
}

/*******************************************************************
 * Usage Example
 *******************************************************************/
async function main() {
  // Suppose you have an existing transaction where the sender
  // already signed for a dynamic fee transaction
  // (Here we just fill in some dummy fields.)
  const senderTx = new FeeDelegatedDynamicFeeTx({
    chainId: 8217,  // Example chainId for Klaytn mainnet, etc.
    nonce: 123,
    gasTipCap: 0,
    gasFeeCap: 25000000000,
    gasLimit: 21000,
    to: "0x0123456789012345678901234567890123456789",
    value: ethers.utils.parseEther("1.0"),
    data: "0x",
    senderV: 0x1b, // For demonstration
    senderR: "0x1111111111111111111111111111111111111111111111111111111111111111",
    senderS: "0x2222222222222222222222222222222222222222222222222222222222222222",
    feePayer: null, // We'll fill this in with the fee payer’s address
  });

  // The fee payer’s private key
  const feePayerPrivateKey = "0x3333333333333333333333333333333333333333333333333333333333333333";

  // Call our signFeeDelegationTx function
  const feeDelegatedTx = await signFeeDelegationTx(senderTx, feePayerPrivateKey);

  // Now we can serialize the fully signed transaction (sender + fee payer)
  const rawTx = feeDelegatedTx.serialize();

  console.log("Serialized fee-delegated transaction:", rawTx);

  // In a real environment, you would send rawTx to a Klaytn node, e.g.:
  // const provider = new ethers.providers.JsonRpcProvider("https://public-node-api.klaytnapi.com/v1/cypress");
  // const txHash = await provider.sendTransaction(rawTx);
  // console.log("Klaytn Tx Hash:", txHash.hash);
}

main().catch((err) => console.error(err));

© 2025 NEXUS Co., Ltd. All Rights Reserved.