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));
Updated 26 days ago