I am attempting to replicate the following scenario using the Hedera JavaScript SDK
This scenario involves the following steps:
TokenCreateTransaction
)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 the setTransactionId()
method as shown below:
const transaction = new TokenCreateTransaction()
.setTransactionId(transactionId)
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 the generate()
method from the TransactionId class
. Make sure that the class is imported at the beginning of the code:
import { TransactionId } from "@hashgraph/sdk";
const transactionId = TransactionId.generate(aliceAccountId)
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.
Important: this flow requires that Bob knows Alice's AccountId before creating the transaction.
Here is all the updated code (only 2 lines are added, marked with a comment as: <--- New Line
):
import {
AccountId,
TokenCreateTransaction,
TokenType,
PrivateKey,
Client,
TransactionId, // <--- New Line
} from "@hashgraph/sdk";
// --- BOB Account (SIGNER) ---
const BOB_ACCOUNTID = "0.0.5782085";
const BOB_PRIVATE_KEY = "";
// --- ALICE Account (PAYER) ---
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`)
// and sets a specific transactionId to designate Alice as the payer account
const transaction = new TokenCreateTransaction()
.setTokenName("New Token 123")
.setTokenSymbol("NT123")
.setTokenType(TokenType.FungibleCommon)
.setInitialSupply(2000)
.setTreasuryAccountId(bobAccountId)
.setTransactionId(TransactionId.generate(aliceAccountId)); // <--- New Line
// 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 doubleSignedTx = await signedTx.sign(alicePrivateKey);
// 4. Alice executes the transaction and pays for the transaction fees
const txResponse = await doubleSignedTx.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();
You can see here the result where Alice (AccountId 0.0.1079726
) is the payer account: https://hashscan.io/testnet/transaction/1698423106.148047003
Hope it will be helpful to someone!