Repository: algorand/js-algorand-sdk
Pull request #451 (2021-09-30 19:43:28)

Support deserializing nonpart and offline keyreg
This PR extends
    https://github.com/algorand/js-algorand-sdk/pull/450
adding support for offline key registration transactions too.

Previously offline key registration would not be deserializable.


Patch:
diff --git a/src/transaction.ts b/src/transaction.ts
index ad43b62..3aa0a83 100644
--- a/src/transaction.ts
+++ b/src/transaction.ts
@@ -430,6 +430,7 @@ export class Transaction implements TransactionStorageStructure {
     if (txn.selectionKey !== undefined) {
       txn.selectionKey = Buffer.from(txn.selectionKey as string, 'base64');
     }
+    // Checking non-participation key registration
     if (
       txn.nonParticipation &&
       (txn.voteKey ||
@@ -442,6 +443,30 @@ export class Transaction implements TransactionStorageStructure {
         'nonParticipation is true but participation params are present.'
       );
     }
+    // Checking online key registration
+    if (
+      !txn.nonParticipation &&
+      (txn.voteKey ||
+        txn.selectionKey ||
+        txn.voteFirst ||
+        txn.voteLast ||
+        txn.voteKeyDilution) &&
+      !(
+        txn.voteKey &&
+        txn.selectionKey &&
+        txn.voteFirst &&
+        txn.voteLast &&
+        txn.voteKeyDilution
+      )
+    ) {
+      throw new Error(
+        'online key registration missing at least one of the following fields: ' +
+          'voteKey, selectionKey, voteFirst, voteLast, voteKeyDilution'
+      );
+    }
+    // The last option is an offline key registration where all the fields
+    // nonParticipation, voteKey, selectionKey, voteFirst, voteLast, voteKeyDilution
+    // are all undefined/false
 
     // Remove unwanted properties and store transaction on instance
     delete ((txn as unknown) as AnyTransactionWithParams).suggestedParams;
@@ -530,12 +555,12 @@ export class Transaction implements TransactionStorageStructure {
       }
       if (this.nonParticipation) {
         txn.nonpart = true;
-        delete txn.votekey;
-        delete txn.selkey;
-        delete txn.votefst;
-        delete txn.votelst;
-        delete txn.votekd;
       }
+      if (!txn.selkey) delete txn.selkey;
+      if (!txn.votekey) delete txn.votekey;
+      if (!txn.votefst) delete txn.votefst;
+      if (!txn.votelst) delete txn.votelst;
+      if (!txn.votekd) delete txn.votekd;
       return txn;
     }
     if (this.type === 'acfg') {
@@ -790,12 +815,24 @@ export class Transaction implements TransactionStorageStructure {
           address.encodeAddress(txnForEnc.close)
         );
     } else if (txnForEnc.type === 'keyreg') {
-      txn.voteKey = Buffer.from(txnForEnc.votekey);
-      txn.selectionKey = Buffer.from(txnForEnc.selkey);
-      txn.voteKeyDilution = txnForEnc.votekd;
-      txn.voteFirst = txnForEnc.votefst;
-      txn.voteLast = txnForEnc.votelst;
-      txn.nonParticipation = txnForEnc.nonpart;
+      if (txnForEnc.votekey !== undefined) {
+        txn.voteKey = Buffer.from(txnForEnc.votekey);
+      }
+      if (txnForEnc.selkey !== undefined) {
+        txn.selectionKey = Buffer.from(txnForEnc.selkey);
+      }
+      if (txnForEnc.votekd !== undefined) {
+        txn.voteKeyDilution = txnForEnc.votekd;
+      }
+      if (txnForEnc.votefst !== undefined) {
+        txn.voteFirst = txnForEnc.votefst;
+      }
+      if (txnForEnc.votelst !== undefined) {
+        txn.voteLast = txnForEnc.votelst;
+      }
+      if (txnForEnc.nonpart !== undefined) {
+        txn.nonParticipation = txnForEnc.nonpart;
+      }
     } else if (txnForEnc.type === 'acfg') {
       // asset creation, or asset reconfigure, or asset destruction
       if (txnForEnc.caid !== undefined) {


Test patch:
diff --git a/tests/5.Transaction.js b/tests/5.Transaction.js
index 487f74d..48ceecb 100644
--- a/tests/5.Transaction.js
+++ b/tests/5.Transaction.js
@@ -366,6 +366,68 @@ describe('Sign', () => {
       assert.deepStrictEqual(reencRep, encRep);
     });
 
+    it('should correctly serialize and deserialize an offline key registration transaction from msgpack representation', () => {
+      const o = {
+        from: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU',
+        fee: 10,
+        firstRound: 51,
+        lastRound: 61,
+        note: new Uint8Array([123, 12, 200]),
+        genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=',
+        genesisID: '',
+        type: 'keyreg',
+      };
+      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('should correctly serialize and deserialize an offline key registration transaction from msgpack representation with explicit nonParticipation=false', () => {
+      const o = {
+        from: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU',
+        fee: 10,
+        firstRound: 51,
+        lastRound: 61,
+        note: new Uint8Array([123, 12, 200]),
+        genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=',
+        genesisID: '',
+        nonParticipation: false,
+        type: 'keyreg',
+      };
+      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('should correctly serialize and deserialize a nonparticipating key registration transaction from msgpack representation', () => {
+      const o = {
+        from: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU',
+        fee: 10,
+        firstRound: 51,
+        lastRound: 61,
+        note: new Uint8Array([123, 12, 200]),
+        genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=',
+        nonParticipation: true,
+        genesisID: '',
+        type: 'keyreg',
+      };
+      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('should correctly serialize and deserialize an asset configuration transaction from msgpack representation', () => {
       const address =
         'BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4';
@@ -777,6 +839,70 @@ describe('Sign', () => {
       assert.deepStrictEqual(expectedTxn, actualTxn);
     });
 
+    it('should be able to use helper to make an offline keyreg transaction', () => {
+      const from = 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU';
+      const fee = 10;
+      const firstRound = 51;
+      const lastRound = 61;
+      const note = new Uint8Array([123, 12, 200]);
+      const genesisHash = 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=';
+      const genesisID = '';
+      const rekeyTo =
+        'GAQVB24XEPYOPBQNJQAE4K3OLNYTRYD65ZKR3OEW5TDOOGL7MDKABXHHTM';
+      const voteKey = undefined;
+      const selectionKey = undefined;
+      const voteKeyDilution = undefined;
+      const voteFirst = undefined;
+      const voteLast = undefined;
+      const o = {
+        from,
+        fee,
+        firstRound,
+        lastRound,
+        note,
+        genesisHash,
+        voteKey,
+        selectionKey,
+        voteFirst,
+        voteLast,
+        voteKeyDilution,
+        genesisID,
+        reKeyTo: rekeyTo,
+        type: 'keyreg',
+        nonParticipation: false,
+      };
+
+      assert.throws(
+        () =>
+          new algosdk.Transaction({
+            ...o,
+            voteKey: '5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKE=',
+          }),
+        new Error(
+          'online key registration missing at least one of the following fields: ' +
+            'voteKey, selectionKey, voteFirst, voteLast, voteKeyDilution'
+        )
+      );
+
+      const expectedTxn = new algosdk.Transaction(o);
+      const actualTxn = algosdk.makeKeyRegistrationTxn(
+        from,
+        fee,
+        firstRound,
+        lastRound,
+        note,
+        genesisHash,
+        genesisID,
+        voteKey,
+        selectionKey,
+        voteFirst,
+        voteLast,
+        voteKeyDilution,
+        rekeyTo
+      );
+      assert.deepStrictEqual(expectedTxn, actualTxn);
+    });
+
     it('should be able to use helper to make a nonparticipating keyreg transaction', () => {
       const from = 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU';
       const fee = 10;