Repository: algorand/js-algorand-sdk
Pull request #397 (2021-07-07 18:27:00)

Support transactions with a first round of zero
This PR fixes a bug that incorrectly prohibited encoding a transaction with a first round of zero. When using the new devMode flag, rounds are incremented by transactions instead of the flow of time and so it is often the case that the last round (and therefore the first valid round) received from the transaction params endpoint is 0.

To support that use case, this PR adds the first valid round property to the list of transaction properties that are allowed to be zero.


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 3226224..526720b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -28,6 +28,10 @@ module.exports = {
       'always',
       { exceptAfterSingleLine: true },
     ],
+    'no-unused-vars': 'off',
+    '@typescript-eslint/no-unused-vars': ['error'],
+    'no-redeclare': 'off',
+    '@typescript-eslint/no-redeclare': ['error'],
   },
   overrides: [
     {
diff --git a/src/client/client.ts b/src/client/client.ts
index 8598664..20f267d 100644
--- a/src/client/client.ts
+++ b/src/client/client.ts
@@ -70,11 +70,9 @@ type Query<F> = {
  * requested format.
  * @param query
  */
-/* eslint-disable no-redeclare,no-unused-vars */
 function getAcceptFormat(
   query?: Query<'msgpack' | 'json'>
 ): 'application/msgpack' | 'application/json' {
-  /* eslint-enable no-redeclare,no-unused-vars */
   if (
     query !== undefined &&
     Object.prototype.hasOwnProperty.call(query, 'format')
diff --git a/src/encoding/uint64.ts b/src/encoding/uint64.ts
index 81e8946..3dc6e1a 100644
--- a/src/encoding/uint64.ts
+++ b/src/encoding/uint64.ts
@@ -37,7 +37,6 @@ export function encodeUint64(num: number | bigint) {
  * @returns The integer that was encoded in the input data. The return type will
  *   be determined by the parameter decodingMode.
  */
-/* eslint-disable  no-unused-vars,no-redeclare */
 export function decodeUint64(data: Uint8Array, decodingMode: 'safe'): number;
 export function decodeUint64(
   data: Uint8Array,
@@ -45,7 +44,6 @@ export function decodeUint64(
 ): number | bigint;
 export function decodeUint64(data: Uint8Array, decodingMode: 'bigint'): bigint;
 export function decodeUint64(data: any, decodingMode: any = 'safe') {
-  /* eslint-enable  no-unused-vars,no-redeclare */
   if (
     decodingMode !== 'safe' &&
     decodingMode !== 'mixed' &&
diff --git a/src/makeTxn.ts b/src/makeTxn.ts
index 07ccce7..4592016 100644
--- a/src/makeTxn.ts
+++ b/src/makeTxn.ts
@@ -158,7 +158,6 @@ export function makePaymentTxnWithSuggestedParamsFromObject(
  * @param nonParticipation - configure whether the address wants to stop participating. If true,
  *   voteKey, selectionKey, voteFirst, voteLast, and voteKeyDilution must be undefined.
  */
-/* eslint-disable no-unused-vars,no-redeclare */
 export function makeKeyRegistrationTxnWithSuggestedParams(
   from: KeyRegistrationTxn['from'],
   note: KeyRegistrationTxn['note'],
@@ -195,7 +194,6 @@ export function makeKeyRegistrationTxnWithSuggestedParams(
   rekeyTo?: any,
   nonParticipation = false
 ) {
-  /* eslint-enable no-unused-vars,no-redeclare */
   const o: KeyRegistrationTxn = {
     from,
     note,
@@ -234,7 +232,6 @@ export function makeKeyRegistrationTxnWithSuggestedParams(
  *   voteKey, selectionKey, voteFirst, voteLast, and voteKeyDilution must be undefined.
  * @Deprecated in version 2.0 this will change to use the "WithSuggestedParams" signature.
  */
-/* eslint-disable no-unused-vars,no-redeclare */
 export function makeKeyRegistrationTxn(
   from: KeyRegistrationTxn['from'],
   fee: MustHaveSuggestedParamsInline<KeyRegistrationTxn>['fee'],
@@ -283,7 +280,6 @@ export function makeKeyRegistrationTxn(
   rekeyTo?: any,
   nonParticipation: any = false
 ) {
-  /* eslint-enable no-unused-vars,no-redeclare */
   const suggestedParams: SuggestedParams = {
     genesisHash,
     genesisID,
@@ -306,7 +302,6 @@ export function makeKeyRegistrationTxn(
 }
 
 // helper for above makeKeyRegistrationTxnWithSuggestedParams, instead accepting an arguments object
-/* eslint-disable no-unused-vars,no-redeclare */
 export function makeKeyRegistrationTxnWithSuggestedParamsFromObject(
   o: Pick<
     RenameProperty<
@@ -340,7 +335,6 @@ export function makeKeyRegistrationTxnWithSuggestedParamsFromObject(
   }
 ): txnBuilder.Transaction;
 export function makeKeyRegistrationTxnWithSuggestedParamsFromObject(o: any) {
-  /* eslint-enable no-unused-vars,no-redeclare */
   return makeKeyRegistrationTxnWithSuggestedParams(
     o.from,
     o.note,
diff --git a/src/multisig.ts b/src/multisig.ts
index 0213f5e..c6d1930 100644
--- a/src/multisig.ts
+++ b/src/multisig.ts
@@ -85,7 +85,7 @@ export function createMultisigTransaction(
  * MultisigTransaction is a Transaction that also supports creating partially-signed multisig transactions.
  */
 export class MultisigTransaction extends txnBuilder.Transaction {
-  /* eslint-disable class-methods-use-this,no-unused-vars,no-dupe-class-members */
+  /* eslint-disable class-methods-use-this,@typescript-eslint/no-unused-vars,no-dupe-class-members */
   /**
    * Override inherited method to throw an error, as mutating transactions are prohibited in this context
    */
@@ -107,7 +107,7 @@ export class MultisigTransaction extends txnBuilder.Transaction {
   signTxn(sk: any): any {
     throw new Error(MULTISIG_USE_PARTIAL_SIGN_ERROR_MSG);
   }
-  /* eslint-enable class-methods-use-this,no-unused-vars,no-dupe-class-members */
+  /* eslint-enable class-methods-use-this,@typescript-eslint/no-unused-vars,no-dupe-class-members */
 
   /**
    * partialSignTxn partially signs this transaction and returns a partially-signed multisig transaction,
diff --git a/src/transaction.ts b/src/transaction.ts
index f850e9b..4f5c62b 100644
--- a/src/transaction.ts
+++ b/src/transaction.ts
@@ -491,6 +491,7 @@ export class Transaction implements TransactionStorageStructure {
       if (!txn.note.length) delete txn.note;
       if (!txn.amt) delete txn.amt;
       if (!txn.fee) delete txn.fee;
+      if (!txn.fv) delete txn.fv;
       if (!txn.gen) delete txn.gen;
       if (txn.grp === undefined) delete txn.grp;
       if (!txn.lx.length) delete txn.lx;
@@ -519,6 +520,7 @@ export class Transaction implements TransactionStorageStructure {
       if (!txn.note.length) delete txn.note;
       if (!txn.lx.length) delete txn.lx;
       if (!txn.fee) delete txn.fee;
+      if (!txn.fv) delete txn.fv;
       if (!txn.gen) delete txn.gen;
       if (txn.grp === undefined) delete txn.grp;
       if (this.reKeyTo !== undefined) {
@@ -573,6 +575,7 @@ export class Transaction implements TransactionStorageStructure {
       if (!txn.lx.length) delete txn.lx;
       if (!txn.amt) delete txn.amt;
       if (!txn.fee) delete txn.fee;
+      if (!txn.fv) delete txn.fv;
       if (!txn.gen) delete txn.gen;
       if (this.reKeyTo !== undefined) {
         txn.rekey = Buffer.from(this.reKeyTo.publicKey);
@@ -637,6 +640,7 @@ export class Transaction implements TransactionStorageStructure {
       if (!txn.aamt) delete txn.aamt;
       if (!txn.amt) delete txn.amt;
       if (!txn.fee) delete txn.fee;
+      if (!txn.fv) delete txn.fv;
       if (!txn.gen) delete txn.gen;
       if (txn.grp === undefined) delete txn.grp;
       if (!txn.aclose) delete txn.aclose;
@@ -670,6 +674,7 @@ export class Transaction implements TransactionStorageStructure {
       if (!txn.lx.length) delete txn.lx;
       if (!txn.amt) delete txn.amt;
       if (!txn.fee) delete txn.fee;
+      if (!txn.fv) delete txn.fv;
       if (!txn.gen) delete txn.gen;
       if (!txn.afrz) delete txn.afrz;
       if (txn.grp === undefined) delete txn.grp;
@@ -727,6 +732,7 @@ export class Transaction implements TransactionStorageStructure {
       if (!txn.lx.length) delete txn.lx;
       if (!txn.amt) delete txn.amt;
       if (!txn.fee) delete txn.fee;
+      if (!txn.fv) delete txn.fv;
       if (!txn.gen) delete txn.gen;
       if (!txn.apid) delete txn.apid;
       if (!txn.apls.nui) delete txn.apls.nui;


Test patch:
diff --git a/tests/5.Transaction.js b/tests/5.Transaction.js
index 920059b..339404d 100644
--- a/tests/5.Transaction.js
+++ b/tests/5.Transaction.js
@@ -472,6 +472,30 @@ describe('Sign', () => {
       assert.deepStrictEqual(reencRep, encRep);
     });
 
+    it('should correctly serialize and deserialize a first round of 0', () => {
+      const address =
+        'BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4';
+      const o = {
+        from: address,
+        fee: 10,
+        firstRound: 0,
+        lastRound: 1000,
+        genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=',
+        type: 'afrz',
+        freezeAccount: address,
+        assetIndex: 1,
+        freezeState: true,
+      };
+
+      const expectedTxn = new algosdk.Transaction(o);
+      const encRep = expectedTxn.get_obj_for_encoding();
+      const encTxn = algosdk.encodeObj(encRep);
+      const decEncRep = algosdk.decodeObj(encTxn);
+      const decTxn = algosdk.Transaction.from_obj_for_encoding(decEncRep);
+      const reencRep = decTxn.get_obj_for_encoding();
+      assert.deepStrictEqual(reencRep, encRep);
+    });
+
     it('reserializes correctly no genesis ID', () => {
       const o = {
         from: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU',
diff --git a/tests/6.Multisig.js b/tests/6.Multisig.js
index 2a86a45..cdadacc 100644
--- a/tests/6.Multisig.js
+++ b/tests/6.Multisig.js
@@ -21,7 +21,7 @@ describe('Multisig Functionality', () => {
           ).publicKey,
         ],
       };
-      /* eslint-disable no-unused-vars */
+      /* eslint-disable @typescript-eslint/no-unused-vars */
       const multisigAddr =
         'RWJLJCMQAFZ2ATP2INM2GZTKNL6OULCCUBO5TQPXH3V2KR4AG7U5UA5JNM';
       const mnem1 =
@@ -30,7 +30,7 @@ describe('Multisig Functionality', () => {
         'since during average anxiety protect cherry club long lawsuit loan expand embark forum theory winter park twenty ball kangaroo cram burst board host ability left';
       const mnem3 =
         'advice pudding treat near rule blouse same whisper inner electric quit surface sunny dismiss leader blood seat clown cost exist hospital century reform able sponsor';
-      /* eslint-enable no-unused-vars */
+      /* eslint-enable @typescript-eslint/no-unused-vars */
 
       const o = {
         snd: Buffer.from(
@@ -93,7 +93,7 @@ describe('Multisig Functionality', () => {
           ).publicKey,
         ],
       };
-      /* eslint-disable no-unused-vars */
+      /* eslint-disable @typescript-eslint/no-unused-vars */
       const multisigAddr =
         'RWJLJCMQAFZ2ATP2INM2GZTKNL6OULCCUBO5TQPXH3V2KR4AG7U5UA5JNM';
       const mnem1 =
@@ -102,7 +102,7 @@ describe('Multisig Functionality', () => {
         'since during average anxiety protect cherry club long lawsuit loan expand embark forum theory winter park twenty ball kangaroo cram burst board host ability left';
       const mnem3 =
         'advice pudding treat near rule blouse same whisper inner electric quit surface sunny dismiss leader blood seat clown cost exist hospital century reform able sponsor';
-      /* eslint-enable no-unused-vars */
+      /* eslint-enable @typescript-eslint/no-unused-vars */
 
       const o = {
         snd: Buffer.from(
diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js
index b9e7bf9..bc63703 100644
--- a/tests/cucumber/steps/steps.js
+++ b/tests/cucumber/steps/steps.js
@@ -1835,7 +1835,7 @@ module.exports = function getSteps(options) {
 
   When(
     'we make any {string} call to {string}.',
-    // eslint-disable-next-line no-unused-vars
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
     async function (client, _endpoint) {
       try {
         if (client === 'algod') {