Repository: algorand/js-algorand-sdk
Pull request #384 (2021-06-21 21:02:33)

Fee pooling SDK change
This PR is to support creating transactions with a fee under mintxnfee since other txns in the group will be paying.


Patch:
diff --git a/Makefile b/Makefile
index afcb06a..afe70db 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 unit:
-	node_modules/.bin/cucumber-js --tags "@unit.offline or @unit.algod or @unit.indexer or @unit.rekey or @unit.tealsign or @unit.dryrun or @unit.applications or @unit.responses or @unit.transactions or @unit.responses.231" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
+	node_modules/.bin/cucumber-js --tags "@unit.offline or @unit.algod or @unit.indexer or @unit.rekey or @unit.tealsign or @unit.dryrun or @unit.applications or @unit.responses or @unit.transactions or @unit.responses.231 or @unit.feetest" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
 integration:
 	node_modules/.bin/cucumber-js --tags "@algod or @assets or @auction or @kmd or @send or @template or @indexer or @rekey or @dryrun or @compile or @applications or @indexer.applications or @applications.verified or @indexer.231" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
 
diff --git a/src/main.ts b/src/main.ts
index 3022161..3aa70dd 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -26,7 +26,7 @@ export const MULTISIG_BAD_SENDER_ERROR_MSG =
  * Key registration fields: fee, firstRound, lastRound, voteKey, selectionKey, voteFirst,
  * voteLast, voteKeyDilution, genesisHash, note(optional), GenesisID(optional)
  *
- * If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ * If flatFee is not set and the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
  * @param txn - object with either payment or key registration fields
  * @param sk - Algorand Secret Key
  * @returns object contains the binary signed transaction and its txID
diff --git a/src/makeTxn.ts b/src/makeTxn.ts
index 2d25f40..07ccce7 100644
--- a/src/makeTxn.ts
+++ b/src/makeTxn.ts
@@ -35,7 +35,7 @@ import { RenameProperties, RenameProperty } from './types/utils';
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *      If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -149,7 +149,7 @@ export function makePaymentTxnWithSuggestedParamsFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -374,7 +374,7 @@ export function makeKeyRegistrationTxnWithSuggestedParamsFromObject(o: any) {
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -559,7 +559,7 @@ export function makeAssetCreateTxnWithSuggestedParamsFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -711,7 +711,7 @@ export function makeAssetConfigTxnWithSuggestedParamsFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -809,7 +809,7 @@ export function makeAssetDestroyTxnWithSuggestedParamsFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -931,7 +931,9 @@ export function makeAssetFreezeTxnWithSuggestedParamsFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
+ * * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -1060,7 +1062,7 @@ export function makeAssetTransferTxnWithSuggestedParamsFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -1189,7 +1191,7 @@ export function makeApplicationCreateTxnFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -1288,7 +1290,7 @@ export function makeApplicationUpdateTxnFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -1375,7 +1377,7 @@ export function makeApplicationDeleteTxnFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -1462,7 +1464,7 @@ export function makeApplicationOptInTxnFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -1549,7 +1551,7 @@ export function makeApplicationCloseOutTxnFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
@@ -1636,7 +1638,7 @@ export function makeApplicationClearStateTxnFromObject(
  * @param suggestedParams - a dict holding common-to-all-txns args:
  * fee - integer fee per byte, in microAlgos. for a flat fee, set flatFee to true
  * flatFee - bool optionally set this to true to specify fee as microalgos-per-txn
- *  If the final calculated fee is lower than the protocol minimum fee, the fee will be increased to match the minimum.
+ *       If true, txn fee may fall below the ALGORAND_MIN_TX_FEE
  * firstRound - integer first protocol round on which this txn is valid
  * lastRound - integer last protocol round on which this txn is valid
  * genesisHash - string specifies hash genesis block of network in use
diff --git a/src/transaction.ts b/src/transaction.ts
index fd25f61..c52b89f 100644
--- a/src/transaction.ts
+++ b/src/transaction.ts
@@ -447,10 +447,10 @@ export class Transaction implements TransactionStorageStructure {
     // Modify Fee
     if (!txn.flatFee) {
       this.fee *= this.estimateSize();
-    }
-    // If suggested fee too small and will be rejected, set to min tx fee
-    if (this.fee < ALGORAND_MIN_TX_FEE) {
-      this.fee = ALGORAND_MIN_TX_FEE;
+      // If suggested fee too small and will be rejected, set to min tx fee
+      if (this.fee < ALGORAND_MIN_TX_FEE) {
+        this.fee = ALGORAND_MIN_TX_FEE;
+      }
     }
 
     // say we are aware of groups


Test patch:
diff --git a/tests/5.Transaction.js b/tests/5.Transaction.js
index d34febe..920059b 100644
--- a/tests/5.Transaction.js
+++ b/tests/5.Transaction.js
@@ -92,9 +92,43 @@ describe('Sign', () => {
       note: new Uint8Array([123, 12, 200]),
     };
     const txn = new algosdk.Transaction(o);
-    assert.equal(txn.fee, 1000); // 1000 is the v5 min txn fee
+    assert.strictEqual(txn.fee, 1000); // 1000 is the v5 min txn fee
     const txnEnc = txn.get_obj_for_encoding();
-    assert.equal(txnEnc.fee, 1000);
+    assert.strictEqual(txnEnc.fee, 1000);
+  });
+
+  it('should accept 0 fee', () => {
+    const o = {
+      from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q',
+      to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q',
+      fee: 0,
+      flatFee: true,
+      amount: 847,
+      firstRound: 51,
+      lastRound: 61,
+      genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=',
+      note: new Uint8Array([123, 12, 200]),
+    };
+    const txn = new algosdk.Transaction(o);
+    assert.equal(txn.fee, 0);
+  });
+
+  it('should accept lower than min fee', () => {
+    const o = {
+      from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q',
+      to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q',
+      fee: 10,
+      flatFee: true,
+      amount: 847,
+      firstRound: 51,
+      lastRound: 61,
+      genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=',
+      note: new Uint8Array([123, 12, 200]),
+    };
+    const txn = new algosdk.Transaction(o);
+    assert.equal(txn.fee, 10);
+    const txnEnc = txn.get_obj_for_encoding();
+    assert.equal(txnEnc.fee, 10);
   });
 
   it('should not complain on a missing genesisID', () => {
diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js
index 56a3b91..b9e7bf9 100644
--- a/tests/cucumber/steps/steps.js
+++ b/tests/cucumber/steps/steps.js
@@ -724,6 +724,31 @@ module.exports = function getSteps(options) {
     return this.txn;
   });
 
+  When('I create the multisig payment transaction with zero fee', function () {
+    this.txn = {
+      from: algosdk.multisigAddress(this.msig),
+      to: this.to,
+      fee: this.fee,
+      flatFee: true,
+      firstRound: this.fv,
+      lastRound: this.lv,
+      genesisHash: this.gh,
+    };
+    if (this.gen) {
+      this.txn.genesisID = this.gen;
+    }
+    if (this.close) {
+      this.txn.closeRemainderTo = this.close;
+    }
+    if (this.note) {
+      this.txn.note = this.note;
+    }
+    if (this.amt) {
+      this.txn.amount = this.amt;
+    }
+    return this.txn;
+  });
+
   When('I send the transaction', async function () {
     this.txid = await this.acl.sendRawTransaction(this.stx);
     this.txid = this.txid.txId;
@@ -4421,6 +4446,18 @@ module.exports = function getSteps(options) {
     }
   );
 
+  Then('fee field is in txn', async function () {
+    const s = algosdk.decodeObj(this.stx);
+    const { txn } = s;
+    assert.strictEqual('fee' in txn, true);
+  });
+
+  Then('fee field not in txn', async function () {
+    const s = algosdk.decodeObj(this.stx);
+    const { txn } = s;
+    assert.strictEqual(!('fee' in txn), true);
+  });
+
   if (!options.ignoreReturn) {
     return steps;
   }