From 71cfc6d595ef7a8c7adcf11a6f9841f3e9bc8bc5 Mon Sep 17 00:00:00 2001 From: jairajdev Date: Fri, 8 May 2026 21:36:45 +0800 Subject: [PATCH 1/2] feat: support recipient-paid transfer tx fee - add recipientPaysTxFee as an optional transfer payload field - gate recipient-paid fee behavior behind the 2.4.9 supportRecipientPaysTxFee version flag - deduct successful transfer tx fees from the recipient when opted in - keep failed transfer tx fees charged to the sender --- docs/API.md | 9 +++++--- src/@types/index.ts | 1 + src/@types/transactionSchemas.ts | 2 ++ src/config/index.ts | 2 ++ src/transactions/transfer.ts | 33 ++++++++++++++++++++++++++++-- src/versioning/migrations/2.4.9.ts | 1 + 6 files changed, 43 insertions(+), 5 deletions(-) diff --git a/docs/API.md b/docs/API.md index 9cce4346..dea9d055 100644 --- a/docs/API.md +++ b/docs/API.md @@ -487,6 +487,7 @@ Transfers tokens between accounts. "from": string, // Source account ID "to": string, // Target account ID "amount": bigint, // Amount to transfer (must be > 0) + "recipientPaysTxFee": boolean, // Optional, defaults to false. Supported from version 2.4.9. "timestamp": number, "sign": { "owner": string // Must match 'from' field @@ -499,15 +500,17 @@ Requirements: - Amount must be greater than 0 - Source account must have sufficient balance to cover: - Transfer amount - - Transaction fee + - Transaction fee, unless `recipientPaysTxFee` is `true` - Maintenance amount +- If `recipientPaysTxFee` is `true`, the recipient balance plus transfer amount must cover the transaction fee - Must be signed by the source account - Signature must be cryptographically valid The transaction will: -1. Deduct amount + fees from source account +1. Deduct amount and maintenance fee from source account 2. Add amount to target account -3. Update timestamps for both accounts +3. Deduct transaction fee from source account, or from target account when `recipientPaysTxFee` is `true` +4. Update timestamps for both accounts #### `email` ⚠️ DEPRECATED **Deprecated in version 2.5.0** - This transaction type is deprecated and will be removed in a future version. New transactions of this type will be rejected when network version >= 2.5.0. diff --git a/src/@types/index.ts b/src/@types/index.ts index a02d4e95..0ba6d78d 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -415,6 +415,7 @@ export namespace Tx { } chatId: string fee?: bigint // Optional fee for the transfer + recipientPaysTxFee?: boolean // Optional, defaults to false. If true, recipient pays the tx fee. } export interface Verify extends BaseLiberdusTx { diff --git a/src/@types/transactionSchemas.ts b/src/@types/transactionSchemas.ts index 854456bd..0082c1d2 100644 --- a/src/@types/transactionSchemas.ts +++ b/src/@types/transactionSchemas.ts @@ -98,6 +98,8 @@ export const schemaTransferTX = { amount: { isBigInt: true }, memo: { type: ['string', 'null'] }, chatId: { type: 'string' }, + fee: { isBigInt: true }, + recipientPaysTxFee: { type: 'boolean' }, }, required: [...baseTxRequired, 'from', 'to', 'amount', 'chatId'], additionalProperties: false, diff --git a/src/config/index.ts b/src/config/index.ts index 2a136139..611fe0bc 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -253,6 +253,7 @@ interface LiberdusFlags { weiToLibStringFormat: boolean includeTxToKeyInReadTx: boolean updateTollRequiredTxInChatHistory: boolean + supportRecipientPaysTxFee: boolean } } @@ -298,6 +299,7 @@ export const LiberdusFlags: LiberdusFlags = { weiToLibStringFormat: true, // turn on by 2.4.8 includeTxToKeyInReadTx: true, // turn on by 2.4.8 updateTollRequiredTxInChatHistory: false, // turn on by 2.4.9 + supportRecipientPaysTxFee: false, // turn on by 2.4.9 }, } diff --git a/src/transactions/transfer.ts b/src/transactions/transfer.ts index c08943cb..5b55463d 100644 --- a/src/transactions/transfer.ts +++ b/src/transactions/transfer.ts @@ -37,6 +37,12 @@ export const validate_fields = (tx: Tx.Transfer, response: ShardusTypes.Incoming response.reason = `tx "memo" size must be less than ${config.LiberdusFlags.transferMemoLimit} characters.` return response } + if (config.LiberdusFlags.versionFlags.supportRecipientPaysTxFee === true) { + if (tx.recipientPaysTxFee !== undefined && typeof tx.recipientPaysTxFee !== 'boolean') { + response.reason = 'tx "recipientPaysTxFee" field must be a boolean.' + return response + } + } if (!tx.sign || !tx.sign.owner || !tx.sign.sig || tx.sign.owner !== tx.from) { response.reason = 'not signed by from account' return response @@ -89,10 +95,25 @@ export const validate = ( return response } - if (from.data.balance < tx.amount + utils.getTransactionFeeWei(AccountsStorage.cachedNetworkAccount)) { + const transactionFee = utils.getTransactionFeeWei(AccountsStorage.cachedNetworkAccount) + const recipientPaysTxFee = + config.LiberdusFlags.versionFlags.supportRecipientPaysTxFee === true && + tx.recipientPaysTxFee === true + + if (recipientPaysTxFee === false && from.data.balance < tx.amount + transactionFee) { response.reason = "from account doesn't have sufficient balance to cover the transaction" return response } + if (recipientPaysTxFee === true) { + if (from.data.balance < tx.amount) { + response.reason = "from account doesn't have sufficient balance to cover the transfer amount" + return response + } + if (to.data.balance + tx.amount < transactionFee) { + response.reason = "to account doesn't have sufficient balance to cover the transaction fee" + return response + } + } // if there is a memo, check if the amount is larger than the Toll required for the chat if (config.LiberdusFlags.versionFlags.minTransferAmountCheck) { const hasMemo = (tx.memo && tx.memo.length > 0) || (tx.xmemo && tx.xmemo.message && tx.xmemo.message.length > 0) @@ -160,10 +181,18 @@ export const apply = ( // update balances const transactionFee = utils.getTransactionFeeWei(AccountsStorage.cachedNetworkAccount) const maintenanceFee = utils.maintenanceAmount(txTimestamp, from, network) - from.data.balance = SafeBigIntMath.subtract(from.data.balance, transactionFee) + const recipientPaysTxFee = + config.LiberdusFlags.versionFlags.supportRecipientPaysTxFee === true && + tx.recipientPaysTxFee === true + if (recipientPaysTxFee === false) { + from.data.balance = SafeBigIntMath.subtract(from.data.balance, transactionFee) + } from.data.balance = SafeBigIntMath.subtract(from.data.balance, maintenanceFee) from.data.balance = SafeBigIntMath.subtract(from.data.balance, tx.amount) to.data.balance = SafeBigIntMath.add(to.data.balance, tx.amount) + if (recipientPaysTxFee === true) { + to.data.balance = SafeBigIntMath.subtract(to.data.balance, transactionFee) + } // store transfer data in chat if (!from.data.chats[tx.to]) { diff --git a/src/versioning/migrations/2.4.9.ts b/src/versioning/migrations/2.4.9.ts index 1cda4be1..dd1cccd6 100644 --- a/src/versioning/migrations/2.4.9.ts +++ b/src/versioning/migrations/2.4.9.ts @@ -7,4 +7,5 @@ export const migrate: Migration = async () => { nestedCountersInstance.countEvent('migrate', 'calling migrate 2.4.9') LiberdusFlags.versionFlags.updateTollRequiredTxInChatHistory = true + LiberdusFlags.versionFlags.supportRecipientPaysTxFee = true } From 47502fcfa4fc7bfe311eaed8a6816b905292ce4b Mon Sep 17 00:00:00 2001 From: jairajdev Date: Wed, 13 May 2026 13:52:40 +0800 Subject: [PATCH 2/2] feat: support transfer tx fee deduction from amount - add deductTxFeeFromAmount as an optional transfer payload field - gate the behavior behind the 2.4.9 supportDeductTxFeeFromAmount version flag - deduct successful transfer tx fees from the transfer amount when opted in - document that receipt amount reflects the net amount received when fee is deducted - add a commented client transfer option for deductTxFeeFromAmount --- client.js | 1 + docs/API.md | 13 ++++--- src/@types/index.ts | 2 +- src/@types/transactionSchemas.ts | 2 +- src/config/index.ts | 4 +-- src/transactions/transfer.ts | 56 +++++++++++++++++++----------- src/versioning/migrations/2.4.9.ts | 2 +- 7 files changed, 47 insertions(+), 33 deletions(-) diff --git a/client.js b/client.js index e260ec9f..5089e57e 100644 --- a/client.js +++ b/client.js @@ -1027,6 +1027,7 @@ vorpal.command('transfer', 'transfers tokens to another account').action(async f chatId: calculateChatId(to, USER.address), memo: answers.memo ? answers.memo : null, timestamp: Date.now(), + // deductTxFeeFromAmount: true, //test: false } signTransaction(tx) diff --git a/docs/API.md b/docs/API.md index dea9d055..3763f9ee 100644 --- a/docs/API.md +++ b/docs/API.md @@ -487,7 +487,7 @@ Transfers tokens between accounts. "from": string, // Source account ID "to": string, // Target account ID "amount": bigint, // Amount to transfer (must be > 0) - "recipientPaysTxFee": boolean, // Optional, defaults to false. Supported from version 2.4.9. + "deductTxFeeFromAmount": boolean, // Optional, defaults to false. Supported from version 2.4.9. "timestamp": number, "sign": { "owner": string // Must match 'from' field @@ -500,16 +500,15 @@ Requirements: - Amount must be greater than 0 - Source account must have sufficient balance to cover: - Transfer amount - - Transaction fee, unless `recipientPaysTxFee` is `true` - - Maintenance amount -- If `recipientPaysTxFee` is `true`, the recipient balance plus transfer amount must cover the transaction fee + - Transaction fee, unless `deductTxFeeFromAmount` is `true` +- If `deductTxFeeFromAmount` is `true`, the transfer amount must be greater than the transaction fee - Must be signed by the source account - Signature must be cryptographically valid The transaction will: -1. Deduct amount and maintenance fee from source account -2. Add amount to target account -3. Deduct transaction fee from source account, or from target account when `recipientPaysTxFee` is `true` +1. Deduct amount from source account +2. Add amount to target account, minus transaction fee when `deductTxFeeFromAmount` is `true` +3. Deduct transaction fee from source account when `deductTxFeeFromAmount` is false or omitted 4. Update timestamps for both accounts #### `email` ⚠️ DEPRECATED diff --git a/src/@types/index.ts b/src/@types/index.ts index 0ba6d78d..a19a535d 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -415,7 +415,7 @@ export namespace Tx { } chatId: string fee?: bigint // Optional fee for the transfer - recipientPaysTxFee?: boolean // Optional, defaults to false. If true, recipient pays the tx fee. + deductTxFeeFromAmount?: boolean // Optional, defaults to false. If true, tx fee is deducted from the transfer amount. } export interface Verify extends BaseLiberdusTx { diff --git a/src/@types/transactionSchemas.ts b/src/@types/transactionSchemas.ts index 0082c1d2..06128b2c 100644 --- a/src/@types/transactionSchemas.ts +++ b/src/@types/transactionSchemas.ts @@ -99,7 +99,7 @@ export const schemaTransferTX = { memo: { type: ['string', 'null'] }, chatId: { type: 'string' }, fee: { isBigInt: true }, - recipientPaysTxFee: { type: 'boolean' }, + deductTxFeeFromAmount: { type: 'boolean' }, }, required: [...baseTxRequired, 'from', 'to', 'amount', 'chatId'], additionalProperties: false, diff --git a/src/config/index.ts b/src/config/index.ts index 611fe0bc..82be7b46 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -253,7 +253,7 @@ interface LiberdusFlags { weiToLibStringFormat: boolean includeTxToKeyInReadTx: boolean updateTollRequiredTxInChatHistory: boolean - supportRecipientPaysTxFee: boolean + supportDeductTxFeeFromAmount: boolean } } @@ -299,7 +299,7 @@ export const LiberdusFlags: LiberdusFlags = { weiToLibStringFormat: true, // turn on by 2.4.8 includeTxToKeyInReadTx: true, // turn on by 2.4.8 updateTollRequiredTxInChatHistory: false, // turn on by 2.4.9 - supportRecipientPaysTxFee: false, // turn on by 2.4.9 + supportDeductTxFeeFromAmount: false, // turn on by 2.4.9 }, } diff --git a/src/transactions/transfer.ts b/src/transactions/transfer.ts index 5b55463d..adbef39d 100644 --- a/src/transactions/transfer.ts +++ b/src/transactions/transfer.ts @@ -37,11 +37,18 @@ export const validate_fields = (tx: Tx.Transfer, response: ShardusTypes.Incoming response.reason = `tx "memo" size must be less than ${config.LiberdusFlags.transferMemoLimit} characters.` return response } - if (config.LiberdusFlags.versionFlags.supportRecipientPaysTxFee === true) { - if (tx.recipientPaysTxFee !== undefined && typeof tx.recipientPaysTxFee !== 'boolean') { - response.reason = 'tx "recipientPaysTxFee" field must be a boolean.' + if (config.LiberdusFlags.versionFlags.supportDeductTxFeeFromAmount === true) { + if (tx.deductTxFeeFromAmount !== undefined && typeof tx.deductTxFeeFromAmount !== 'boolean') { + response.reason = 'tx "deductTxFeeFromAmount" field must be a boolean.' return response } + if (tx.deductTxFeeFromAmount === true) { + const transactionFee = utils.getTransactionFeeWei(AccountsStorage.cachedNetworkAccount) + if (tx.amount <= transactionFee) { + response.reason = 'transfer amount must be greater than the transaction fee when deductTxFeeFromAmount is true' + return response + } + } } if (!tx.sign || !tx.sign.owner || !tx.sign.sig || tx.sign.owner !== tx.from) { response.reason = 'not signed by from account' @@ -96,21 +103,21 @@ export const validate = ( } const transactionFee = utils.getTransactionFeeWei(AccountsStorage.cachedNetworkAccount) - const recipientPaysTxFee = - config.LiberdusFlags.versionFlags.supportRecipientPaysTxFee === true && - tx.recipientPaysTxFee === true + const deductTxFeeFromAmount = + config.LiberdusFlags.versionFlags.supportDeductTxFeeFromAmount === true && + tx.deductTxFeeFromAmount === true - if (recipientPaysTxFee === false && from.data.balance < tx.amount + transactionFee) { + if (deductTxFeeFromAmount === false && from.data.balance < tx.amount + transactionFee) { response.reason = "from account doesn't have sufficient balance to cover the transaction" return response } - if (recipientPaysTxFee === true) { + if (deductTxFeeFromAmount === true) { if (from.data.balance < tx.amount) { response.reason = "from account doesn't have sufficient balance to cover the transfer amount" return response } - if (to.data.balance + tx.amount < transactionFee) { - response.reason = "to account doesn't have sufficient balance to cover the transaction fee" + if (tx.amount <= transactionFee) { + response.reason = 'transfer amount must be greater than the transaction fee when deductTxFeeFromAmount is true' return response } } @@ -181,18 +188,19 @@ export const apply = ( // update balances const transactionFee = utils.getTransactionFeeWei(AccountsStorage.cachedNetworkAccount) const maintenanceFee = utils.maintenanceAmount(txTimestamp, from, network) - const recipientPaysTxFee = - config.LiberdusFlags.versionFlags.supportRecipientPaysTxFee === true && - tx.recipientPaysTxFee === true - if (recipientPaysTxFee === false) { + const deductTxFeeFromAmount = + config.LiberdusFlags.versionFlags.supportDeductTxFeeFromAmount === true && + tx.deductTxFeeFromAmount === true + if (deductTxFeeFromAmount === false) { from.data.balance = SafeBigIntMath.subtract(from.data.balance, transactionFee) } from.data.balance = SafeBigIntMath.subtract(from.data.balance, maintenanceFee) from.data.balance = SafeBigIntMath.subtract(from.data.balance, tx.amount) - to.data.balance = SafeBigIntMath.add(to.data.balance, tx.amount) - if (recipientPaysTxFee === true) { - to.data.balance = SafeBigIntMath.subtract(to.data.balance, transactionFee) + let amountReceived = tx.amount + if (deductTxFeeFromAmount === true) { + amountReceived = SafeBigIntMath.subtract(tx.amount, transactionFee) } + to.data.balance = SafeBigIntMath.add(to.data.balance, amountReceived) // store transfer data in chat if (!from.data.chats[tx.to]) { @@ -213,6 +221,15 @@ export const apply = ( to.timestamp = txTimestamp chat.timestamp = txTimestamp + const additionalInfo: { amount: bigint; maintenanceFee: bigint } = { + amount: tx.amount, + maintenanceFee, + } + if (deductTxFeeFromAmount === true) { + // amount reflects the net amount received by the recipient. + additionalInfo.amount = amountReceived + } + const appReceiptData: AppReceiptData = { txId, timestamp: txTimestamp, @@ -221,10 +238,7 @@ export const apply = ( to: tx.to, type: tx.type, transactionFee, - additionalInfo: { - amount: tx.amount, - maintenanceFee, - }, + additionalInfo, } const appReceiptDataHash = crypto.hashObj(appReceiptData) dapp.applyResponseAddReceiptData(applyResponse, appReceiptData, appReceiptDataHash) diff --git a/src/versioning/migrations/2.4.9.ts b/src/versioning/migrations/2.4.9.ts index dd1cccd6..673b4be9 100644 --- a/src/versioning/migrations/2.4.9.ts +++ b/src/versioning/migrations/2.4.9.ts @@ -7,5 +7,5 @@ export const migrate: Migration = async () => { nestedCountersInstance.countEvent('migrate', 'calling migrate 2.4.9') LiberdusFlags.versionFlags.updateTollRequiredTxInChatHistory = true - LiberdusFlags.versionFlags.supportRecipientPaysTxFee = true + LiberdusFlags.versionFlags.supportDeductTxFeeFromAmount = true }