I am attempting to replicate the following scenario using the Hedera JavaScript SDK
- Bob wants to send a transaction to the Hedera network, but he does not have HBAR to cover the transaction fees
- Alice can offer that service to Bob by sending/executing his transaction on the Hedera network, covering the associated transaction fees
This scenario involves the following steps:
- Bob creates the transaction object (e.g. a
TokenCreateTransaction
) - Bob freezes and signs the transaction
- Alice receives the signed transaction from Bob and adds her signature as well
- Alice executes the transaction and pays for the transaction fees
You can see below the implementation:
import {
AccountId,
TokenCreateTransaction,
TokenType,
PrivateKey,
Client,
} from "@hashgraph/sdk";
// --- BOB (SIGNER account) ---
const BOB_ACCOUNTID = "0.0.5782085";
const BOB_PRIVATE_KEY = "";
// --- ALICE (PAYER account) ---
const ALICE_ACCOUNTID = "0.0.1079726";
const ALICE_PRIVATE_KEY = "";
async function main() {
// --- BOB (SIGNER account) ---
const bobPrivateKey = PrivateKey.fromStringED25519(BOB_PRIVATE_KEY);
const bobAccountId = AccountId.fromString(BOB_ACCOUNTID);
const bobClient = Client.forTestnet().setOperator(
bobAccountId,
bobPrivateKey
);
// --- ALICE (PAYER account) ---
const alicePrivateKey = PrivateKey.fromStringED25519(ALICE_PRIVATE_KEY);
const aliceAccountId = AccountId.fromString(ALICE_ACCOUNTID);
const aliceClient = Client.forTestnet().setOperator(
aliceAccountId,
alicePrivateKey
);
// 1. Bob creates the transaction object (e.g. a `TokenCreateTransaction`)
const transaction = new TokenCreateTransaction()
.setTokenName("New Token 123")
.setTokenSymbol("NT123")
.setTokenType(TokenType.FungibleCommon)
.setInitialSupply(2000)
.setTreasuryAccountId(bobAccountId);
// 2. Bob freezes and signs the transaction
const frozenTx = await transaction.freezeWith(bobClient);
const signedTx = await frozenTx.sign(bobPrivateKey);
// 3. Alice receives the signed transaction from Bob and adds her signature
const aliceSignedTx = await signedTx.sign(alicePrivateKey);
// 4. Alice executes the transaction and pays for the transaction fees
const txResponse = await aliceSignedTx.execute(aliceClient);
const receipt = await txResponse.getReceipt(aliceClient);
console.log("TransactionId: " + txResponse.transactionId);
console.log("Transaction status: " + receipt.status.toString());
console.log("Created tokenId: " + receipt.tokenId);
process.exit();
}
main();
The problem arises when Bob freezes the transaction, as certain transaction attributes are automatically modified by the SDK. Specifically, the transactionId
and operatorAccountId
attributes are set based on the freezer account, therefore designating Bob as the payer of the transaction.
So, even though Alice executes the transaction at the end of the process, the payer account is actually Bob.
You can see here the result where Bob (AccountId 0.0.5782085
) is the payer account: https://hashscan.io/testnet/transaction/1698410500.114199003
How can we ensure that Alice is the one who covers the transaction fees, especially when Bob is the initial individual to freeze it? Any idea would be greatly appreciated.
After multiple attempts, I've discovered a solution to the problem.
When Bob is creating the transaction, he needs to set the
transactionId
using thesetTransactionId()
method as shown below:The
transactionId
parameter is crucial in this process. Bob must create it using Alice's AccountId to designate Alice as the payer of the transaction. To generate it, Bob can utilize thegenerate()
method from theTransactionId class
. Make sure that the class is imported at the beginning of the code:In this way, we can ensure that Bob initiates, freezes and sings the transaction while designating Alice as the payer account for covering the transaction fees.
Here is all the updated code (only 2 lines are added, marked with a comment as:
<--- New Line
):You can see here the result where Alice (AccountId
0.0.1079726
) is the payer account: https://hashscan.io/testnet/transaction/1698423106.148047003Hope it will be helpful to someone!