trac-msb 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.github/workflows/acceptance-tests.yml +7 -11
  2. package/.github/workflows/lint-pr-title.yml +26 -0
  3. package/.github/workflows/unit-tests.yml +2 -8
  4. package/CODE_OF_CONDUCT.md +128 -0
  5. package/README.md +33 -18
  6. package/docker-compose.yml +1 -0
  7. package/docs/trac_network_http_api.openapi.yaml +889 -0
  8. package/msb.mjs +5 -22
  9. package/package.json +14 -10
  10. package/proto/network.proto +74 -0
  11. package/rpc/create_server.js +2 -2
  12. package/rpc/handlers.js +165 -92
  13. package/rpc/routes/v1.js +3 -1
  14. package/rpc/rpc_server.js +4 -4
  15. package/rpc/rpc_services.js +62 -25
  16. package/rpc/utils/helpers.js +83 -52
  17. package/src/config/args.js +46 -0
  18. package/src/config/config.js +78 -5
  19. package/src/config/env.js +70 -3
  20. package/src/core/network/Network.js +34 -70
  21. package/src/core/network/identity/NetworkWalletFactory.js +2 -2
  22. package/src/core/network/protocols/LegacyProtocol.js +67 -0
  23. package/src/core/network/protocols/NetworkMessages.js +48 -0
  24. package/src/core/network/protocols/ProtocolInterface.js +31 -0
  25. package/src/core/network/protocols/ProtocolSession.js +59 -0
  26. package/src/core/network/protocols/V1Protocol.js +64 -0
  27. package/src/core/network/protocols/legacy/NetworkMessageRouter.js +84 -0
  28. package/src/core/network/protocols/legacy/handlers/GetRequestHandler.js +53 -0
  29. package/src/core/network/protocols/legacy/handlers/ResponseHandler.js +37 -0
  30. package/src/core/network/{messaging → protocols/legacy}/validators/base/BaseResponse.js +2 -3
  31. package/src/core/network/protocols/shared/handlers/RoleOperationHandler.js +88 -0
  32. package/src/core/network/protocols/shared/handlers/SubnetworkOperationHandler.js +93 -0
  33. package/src/core/network/{messaging → protocols/shared}/handlers/TransferOperationHandler.js +17 -16
  34. package/src/core/network/{messaging → protocols/shared}/handlers/base/BaseOperationHandler.js +10 -15
  35. package/src/core/network/{messaging → protocols/shared}/validators/PartialBootstrapDeployment.js +2 -2
  36. package/src/core/network/{messaging → protocols/shared}/validators/PartialRoleAccess.js +5 -5
  37. package/src/core/network/{messaging → protocols/shared}/validators/PartialTransaction.js +4 -4
  38. package/src/core/network/{messaging → protocols/shared}/validators/PartialTransfer.js +4 -4
  39. package/src/core/network/{messaging → protocols/shared}/validators/base/PartialOperation.js +14 -12
  40. package/src/core/network/protocols/v1/NetworkMessageRouter.js +15 -0
  41. package/src/core/network/services/ConnectionManager.js +5 -5
  42. package/src/core/network/services/MessageOrchestrator.js +2 -2
  43. package/src/core/network/services/TransactionPoolService.js +5 -6
  44. package/src/core/network/services/TransactionRateLimiterService.js +12 -13
  45. package/src/core/network/services/ValidatorObserverService.js +5 -6
  46. package/src/core/state/State.js +3 -5
  47. package/src/index.js +156 -181
  48. package/src/messages/network/v1/NetworkMessageBuilder.js +325 -0
  49. package/src/messages/network/v1/NetworkMessageDirector.js +137 -0
  50. package/src/messages/network/v1/networkMessageFactory.js +12 -0
  51. package/src/messages/state/ApplyStateMessageBuilder.js +661 -0
  52. package/src/messages/state/ApplyStateMessageDirector.js +516 -0
  53. package/src/messages/state/applyStateMessageFactory.js +12 -0
  54. package/src/utils/buffer.js +53 -1
  55. package/src/utils/check.js +1 -1
  56. package/src/utils/cli.js +0 -8
  57. package/src/utils/constants.js +33 -30
  58. package/src/utils/fileUtils.js +13 -0
  59. package/src/utils/normalizers.js +84 -2
  60. package/src/utils/protobuf/network.cjs +840 -0
  61. package/src/utils/protobuf/operationHelpers.js +10 -0
  62. package/src/utils/type.js +26 -0
  63. package/tests/acceptance/v1/balance/balance.test.mjs +1 -2
  64. package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +26 -30
  65. package/tests/acceptance/v1/health/health.test.mjs +33 -0
  66. package/tests/acceptance/v1/rpc.test.mjs +4 -3
  67. package/tests/acceptance/v1/tx/tx.test.mjs +27 -16
  68. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +26 -12
  69. package/tests/fixtures/check.fixtures.js +33 -32
  70. package/tests/fixtures/networkV1.fixtures.js +85 -0
  71. package/tests/fixtures/protobuf.fixtures.js +109 -25
  72. package/tests/helpers/StateNetworkFactory.js +2 -2
  73. package/tests/helpers/address.js +6 -0
  74. package/tests/helpers/autobaseTestHelpers.js +2 -1
  75. package/tests/helpers/config.js +2 -1
  76. package/tests/helpers/setupApplyTests.js +59 -56
  77. package/tests/unit/messages/messages.test.js +12 -0
  78. package/tests/unit/messages/network/NetworkMessageBuilder.test.js +276 -0
  79. package/tests/unit/messages/network/NetworkMessageDirector.test.js +201 -0
  80. package/tests/unit/messages/state/applyStateMessageBuilder.complete.test.js +521 -0
  81. package/tests/unit/messages/state/applyStateMessageBuilder.partial.test.js +233 -0
  82. package/tests/unit/network/ConnectionManager.test.js +6 -5
  83. package/tests/unit/network/networkModule.test.js +3 -2
  84. package/tests/unit/state/apply/addAdmin/addAdminHappyPathScenario.js +10 -6
  85. package/tests/unit/state/apply/addAdmin/addAdminScenarioHelpers.js +9 -6
  86. package/tests/unit/state/apply/addAdmin/state.apply.addAdmin.test.js +10 -7
  87. package/tests/unit/state/apply/addIndexer/addIndexerScenarioHelpers.js +18 -21
  88. package/tests/unit/state/apply/addWriter/addWriterScenarioHelpers.js +53 -38
  89. package/tests/unit/state/apply/adminRecovery/adminRecoveryScenarioHelpers.js +46 -35
  90. package/tests/unit/state/apply/appendWhitelist/appendWhitelistScenarioHelpers.js +13 -16
  91. package/tests/unit/state/apply/balanceInitialization/balanceInitializationScenarioHelpers.js +17 -11
  92. package/tests/unit/state/apply/banValidator/banValidatorScenarioHelpers.js +11 -12
  93. package/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentScenarioHelpers.js +9 -7
  94. package/tests/unit/state/apply/common/commonScenarioHelper.js +15 -14
  95. package/tests/unit/state/apply/common/payload-structure/initializationDisabledScenario.js +9 -4
  96. package/tests/unit/state/apply/disableInitialization/disableInitializationScenarioHelpers.js +17 -11
  97. package/tests/unit/state/apply/removeWriter/removeWriterScenarioHelpers.js +19 -14
  98. package/tests/unit/state/apply/transfer/transferDoubleSpendAcrossValidatorsScenario.js +37 -29
  99. package/tests/unit/state/apply/transfer/transferScenarioHelpers.js +9 -7
  100. package/tests/unit/state/apply/txOperation/txOperationScenarioHelpers.js +11 -9
  101. package/tests/unit/unit.test.js +1 -1
  102. package/tests/unit/utils/buffer/buffer.test.js +62 -1
  103. package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +4 -3
  104. package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +3 -2
  105. package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +3 -2
  106. package/tests/unit/utils/normalizers/normalizers.test.js +469 -0
  107. package/tests/unit/utils/protobuf/operationHelpers.test.js +120 -2
  108. package/tests/unit/utils/type/type.test.js +25 -0
  109. package/tests/unit/utils/utils.test.js +1 -0
  110. package/docs/networking-dualstack-plan.md +0 -75
  111. package/docs/networking-layer-redesign.md +0 -155
  112. package/src/core/network/messaging/NetworkMessages.js +0 -64
  113. package/src/core/network/messaging/handlers/GetRequestHandler.js +0 -113
  114. package/src/core/network/messaging/handlers/ResponseHandler.js +0 -107
  115. package/src/core/network/messaging/handlers/RoleOperationHandler.js +0 -114
  116. package/src/core/network/messaging/handlers/SubnetworkOperationHandler.js +0 -149
  117. package/src/core/network/messaging/routes/NetworkMessageRouter.js +0 -98
  118. package/src/core/network/messaging/validators/AdminResponse.js +0 -58
  119. package/src/core/network/messaging/validators/CustomNodeResponse.js +0 -46
  120. package/src/messages/base/StateBuilder.js +0 -25
  121. package/src/messages/completeStateMessages/CompleteStateMessageBuilder.js +0 -425
  122. package/src/messages/completeStateMessages/CompleteStateMessageDirector.js +0 -252
  123. package/src/messages/completeStateMessages/CompleteStateMessageOperations.js +0 -296
  124. package/src/messages/partialStateMessages/PartialStateMessageBuilder.js +0 -272
  125. package/src/messages/partialStateMessages/PartialStateMessageDirector.js +0 -137
  126. package/src/messages/partialStateMessages/PartialStateMessageOperations.js +0 -138
  127. package/tests/integration/apply/addAdmin/addAdminBasic.test.js +0 -69
  128. package/tests/integration/apply/addAdmin/addAdminRecovery.test.js +0 -126
  129. package/tests/integration/apply/addIndexer.test.js +0 -239
  130. package/tests/integration/apply/addWhitelist.test.js +0 -53
  131. package/tests/integration/apply/addWriter.test.js +0 -245
  132. package/tests/integration/apply/apply.test.js +0 -19
  133. package/tests/integration/apply/banValidator.test.js +0 -116
  134. package/tests/integration/apply/postTx/invalidSubValues.test.js +0 -103
  135. package/tests/integration/apply/postTx/postTx.test.js +0 -196
  136. package/tests/integration/apply/removeIndexer.test.js +0 -132
  137. package/tests/integration/apply/removeWriter.test.js +0 -168
  138. package/tests/integration/apply/transfer.test.js +0 -83
  139. package/tests/integration/integration.test.js +0 -9
  140. package/tests/unit/messageOperations/assembleAddIndexerMessage.test.js +0 -21
  141. package/tests/unit/messageOperations/assembleAddWriterMessage.test.js +0 -17
  142. package/tests/unit/messageOperations/assembleAdminMessage.test.js +0 -68
  143. package/tests/unit/messageOperations/assembleBanWriterMessage.test.js +0 -17
  144. package/tests/unit/messageOperations/assemblePostTransaction.test.js +0 -424
  145. package/tests/unit/messageOperations/assembleRemoveIndexerMessage.test.js +0 -19
  146. package/tests/unit/messageOperations/assembleRemoveWriterMessage.test.js +0 -17
  147. package/tests/unit/messageOperations/assembleWhitelistMessages.test.js +0 -59
  148. package/tests/unit/messageOperations/commonsStateMessageOperationsTest.js +0 -278
  149. package/tests/unit/messageOperations/stateMessageOperations.test.js +0 -19
  150. /package/src/core/network/{messaging → protocols/legacy}/validators/ValidatorResponse.js +0 -0
  151. /package/src/utils/{operations.js → applyOperations.js} +0 -0
@@ -1,6 +1,5 @@
1
1
  import b4a from 'b4a';
2
- import PartialStateMessageOperations from '../../../../../src/messages/partialStateMessages/PartialStateMessageOperations.js';
3
- import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js';
2
+ import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js';
4
3
  import {
5
4
  deriveIndexerSequenceState,
6
5
  eventFlush
@@ -139,20 +138,22 @@ export async function buildTxOperationPayload(
139
138
  writerKeyBuffer = broadcasterPeer.base.local.key
140
139
  } = {}
141
140
  ) {
142
- const resolvedTxValidity = txValidity ?? (await deriveIndexerSequenceState(validatorPeer.base));
141
+ const resolvedTxValidity = txValidity ?? (await deriveIndexerSequenceState(validatorPeer.base));
143
142
 
144
- const partial = await new PartialStateMessageOperations(broadcasterPeer.wallet, config)
145
- .assembleTransactionOperationMessage(
143
+ const partial = await applyStateMessageFactory(broadcasterPeer.wallet, config)
144
+ .buildPartialTransactionOperationMessage(
145
+ broadcasterPeer.wallet.address,
146
146
  writerKeyBuffer.toString('hex'),
147
147
  resolvedTxValidity.toString('hex'),
148
148
  contentHash.toString('hex'),
149
149
  externalBootstrap.toString('hex'),
150
- msbBootstrap.toString('hex')
150
+ msbBootstrap.toString('hex'),
151
+ 'json'
151
152
  );
152
153
 
153
- return new CompleteStateMessageOperations(validatorPeer.wallet, config)
154
- .assembleCompleteTransactionOperationMessage(
155
- partial.address,
154
+ const payload = await applyStateMessageFactory(validatorPeer.wallet, config)
155
+ .buildCompleteTransactionOperationMessage(
156
+ partial.address,
156
157
  b4a.from(partial.txo.tx, 'hex'),
157
158
  b4a.from(partial.txo.txv, 'hex'),
158
159
  b4a.from(partial.txo.iw, 'hex'),
@@ -162,6 +163,7 @@ export async function buildTxOperationPayload(
162
163
  b4a.from(partial.txo.bs, 'hex'),
163
164
  b4a.from(partial.txo.mbs, 'hex')
164
165
  );
166
+ return safeEncodeApplyOperation(payload);
165
167
  }
166
168
 
167
169
  export async function buildTxOperationPayloadWithTxValidity(context, txValidity, options = {}) {
@@ -7,7 +7,7 @@ async function runTests() {
7
7
  await import('./network/networkModule.test.js')
8
8
  await import('./state/stateModule.test.js');
9
9
  await import('./utils/utils.test.js');
10
- // await import('./messageOperations/stateMessageOperations.test.js'); // Broken test - needs fixing
10
+ await import('./messages/messages.test.js');
11
11
  test.resume();
12
12
  }
13
13
 
@@ -1,6 +1,7 @@
1
1
  import test from 'brittle';
2
2
  import b4a from 'b4a';
3
- import {createMessage, isBufferValid, safeWriteUInt32BE, deepCopyBuffer} from '../../../../src/utils/buffer.js';
3
+ import { createMessage, isBufferValid, safeWriteUInt32BE, deepCopyBuffer, encodeCapabilities, timestampToBuffer, idToBuffer } from '../../../../src/utils/buffer.js';
4
+ import { errorMessageIncludes } from "../../../helpers/regexHelper.js";
4
5
 
5
6
  const invalidDataTypes = [
6
7
  null,
@@ -188,3 +189,63 @@ test('deepCopyBuffer - modifying copy does not affect original (is not a referen
188
189
  t.is(buf[0], 9, 'original unchanged');
189
190
  t.is(copy[0], 1, 'copy modified independently');
190
191
  });
192
+
193
+ test('encodeCapabilities - deterministic ordering and encoding', t => {
194
+ const caps = ['cap-b', 'cap-a'];
195
+ const result = encodeCapabilities(caps);
196
+
197
+ const capA = b4a.from('cap-a', 'utf8');
198
+ const capB = b4a.from('cap-b', 'utf8');
199
+
200
+ const expected = b4a.concat([
201
+ b4a.from([0x00, capA.length]),
202
+ capA,
203
+ b4a.from([0x00, capB.length]),
204
+ capB
205
+ ]);
206
+
207
+ t.is(result.length, expected.length, 'length matches');
208
+ t.ok(b4a.equals(result, expected), 'ordering is sorted and encoding matches');
209
+ });
210
+
211
+ test('encodeCapabilities - empty array yields empty buffer', t => {
212
+ const result = encodeCapabilities([]);
213
+ t.ok(b4a.isBuffer(result), 'returns a buffer');
214
+ t.is(result.length, 0, 'empty buffer for no caps');
215
+ });
216
+
217
+ test('encodeCapabilities - throws on invalid input', t => {
218
+ t.exception(
219
+ () => encodeCapabilities('not-array'),
220
+ errorMessageIncludes('Capabilities must be an array')
221
+ );
222
+
223
+ t.exception(
224
+ () => encodeCapabilities([1, 2]),
225
+ errorMessageIncludes('must contain only strings')
226
+ );
227
+ });
228
+
229
+ test('timestampToBuffer - encodes uint64 BE', t => {
230
+ const ts = 2n ** 53n; // beyond uint32
231
+
232
+ const tsBuf = timestampToBuffer(ts);
233
+
234
+ t.is(tsBuf.length, 8);
235
+ t.is(tsBuf.readBigUInt64BE(0), ts);
236
+ });
237
+
238
+ test('idToBuffer - encodes utf8 string', t => {
239
+ const id = 'test-id';
240
+ const idBuf = idToBuffer(id);
241
+ t.ok(b4a.isBuffer(idBuf));
242
+ t.ok(b4a.equals(idBuf, b4a.from(id, 'utf8')));
243
+ });
244
+
245
+ test('timestampToBuffer and idToBuffer - reject invalid input', t => {
246
+ t.exception(() => timestampToBuffer(-1), errorMessageIncludes('timestamp'));
247
+ t.exception(() => timestampToBuffer(1.5), errorMessageIncludes('timestamp'));
248
+ t.exception(() => timestampToBuffer('1'), errorMessageIncludes('timestamp'));
249
+ t.exception.all(() => idToBuffer(1));
250
+ t.exception.all(() => idToBuffer(null));
251
+ });
@@ -4,6 +4,7 @@ import { errorMessageIncludes } from "../../../helpers/regexHelper.js";
4
4
  import fs from 'fs';
5
5
  import PeerWallet from 'trac-wallet';
6
6
  import { config } from '../../../helpers/config.js';
7
+ import { asAddress } from '../../../helpers/address.js';
7
8
 
8
9
  const DUMMY_PATH_OK = './dummy_whitelist_ok.csv';
9
10
  const DUMMY_PATH_DUP = './dummy_whitelist_dup.csv';
@@ -12,8 +13,8 @@ const DUMMY_PATH_BLANK = './dummy_whitelist_blank.csv';
12
13
  const DUMMY_PATH_BOM = './dummy_whitelist_bom.csv';
13
14
  const DUMMY_PATH_LARGE = './dummy_whitelist_large.csv';
14
15
 
15
- const ADDR1 = 'trac1dguwzsvcsehslh6dgj2mqlsxdn7s5t5vhem56yd0xlg47aq6exzqymhr6u';
16
- const ADDR2 = 'trac123z3gfpr2epjwww7ntm3m6ud2fhmq0tvts27p2f5mx3qkecsutlqfys769';
16
+ const ADDR1 = asAddress('6a38e14198866f0fdf4d4495b07e066cfd0a2e8cbe774d11af37d15f741ac984');
17
+ const ADDR2 = asAddress('544514242356432739de9af71deb8d526fb03d6c5c15e0a934d9a20b6710e2fe');
17
18
 
18
19
  hook('Initialize dummy whitelist files', async t => {
19
20
  // Happy path
@@ -91,4 +92,4 @@ hook('Cleanup dummy whitelist files', async t => {
91
92
  [DUMMY_PATH_OK, DUMMY_PATH_DUP, DUMMY_PATH_EMPTY, DUMMY_PATH_BLANK, DUMMY_PATH_BOM, DUMMY_PATH_LARGE].forEach(path => {
92
93
  if (fs.existsSync(path)) fs.unlinkSync(path);
93
94
  });
94
- });
95
+ });
@@ -4,6 +4,7 @@ import { errorMessageIncludes } from "../../../helpers/regexHelper.js";
4
4
  import fs from 'fs';
5
5
  import PeerWallet from 'trac-wallet';
6
6
  import { config } from '../../../helpers/config.js';
7
+ import { asAddress } from '../../../helpers/address.js';
7
8
 
8
9
  const DUMMY_PATH_OK = './dummy_balance_ok.csv';
9
10
  const DUMMY_PATH_DUP = './dummy_balance_dup.csv';
@@ -19,8 +20,8 @@ const DUMMY_PATH_ZERO = './dummy_balance_zero.csv';
19
20
  const DUMMY_PATH_BOM = './dummy_balance_bom.csv';
20
21
  const DUMMY_PATH_LARGE = './dummy_balance_large.csv';
21
22
 
22
- const ADDR1 = 'trac1dguwzsvcsehslh6dgj2mqlsxdn7s5t5vhem56yd0xlg47aq6exzqymhr6u';
23
- const ADDR2 = 'trac123z3gfpr2epjwww7ntm3m6ud2fhmq0tvts27p2f5mx3qkecsutlqfys769';
23
+ const ADDR1 = asAddress('6a38e14198866f0fdf4d4495b07e066cfd0a2e8cbe774d11af37d15f741ac984');
24
+ const ADDR2 = asAddress('544514242356432739de9af71deb8d526fb03d6c5c15e0a934d9a20b6710e2fe');
24
25
 
25
26
  hook('Initialize dummy balance files', async t => {
26
27
  // Happy path
@@ -4,9 +4,10 @@ import { errorMessageIncludes } from "../../../helpers/regexHelper.js";
4
4
  import { ZERO_LICENSE } from '../../../../src/core/state/utils/nodeEntry.js';
5
5
  import b4a from 'b4a';
6
6
  import { config } from '../../../helpers/config.js';
7
+ import { asAddress } from '../../../helpers/address.js';
7
8
 
8
- const VALID_ADDRESS = 'trac1dguwzsvcsehslh6dgj2mqlsxdn7s5t5vhem56yd0xlg47aq6exzqymhr6u';
9
- const ADMIN_ADDRESS = 'trac1yva2pduhz5yst8jgzmrc9ve0as5mx7tcw6le9srj6xcwqkx9hacqxxhsf9';
9
+ const VALID_ADDRESS = asAddress('6a38e14198866f0fdf4d4495b07e066cfd0a2e8cbe774d11af37d15f741ac984');
10
+ const ADMIN_ADDRESS = asAddress('233aa0b7971509059e4816c782b32fec29b3797876bf92c072d1b0e058c5bf70');
10
11
  const INVALID_ADDRESS = 'notanaddress';
11
12
  const LICENSE_NUMBER_ONE = b4a.alloc(4, 1);
12
13
 
@@ -0,0 +1,469 @@
1
+ import { test } from 'brittle';
2
+ import b4a from 'b4a';
3
+
4
+ import { config } from '../../../helpers/config.js';
5
+ import { randomAddress } from '../../state/stateTestUtils.js';
6
+ import { errorMessageIncludes } from '../../../helpers/regexHelper.js';
7
+ import { OperationType } from '../../../../src/utils/constants.js';
8
+ import {
9
+ normalizeBootstrapDeploymentOperation,
10
+ normalizeDecodedPayloadForJson,
11
+ normalizeRoleAccessOperation,
12
+ normalizeTransactionOperation,
13
+ normalizeTransferOperation
14
+ } from '../../../../src/utils/normalizers.js';
15
+ import { addressToBuffer } from '../../../../src/core/state/utils/address.js';
16
+ import { bigIntTo16ByteBuffer } from '../../../../src/utils/amountSerialization.js';
17
+
18
+ const hex = (value, bytes) => value.repeat(bytes);
19
+ const toBuffer = value => b4a.from(value, 'hex');
20
+
21
+ test('normalizeTransferOperation normalizes hex strings and addresses', t => {
22
+ const sender = randomAddress(config.addressPrefix);
23
+ const recipient = randomAddress(config.addressPrefix);
24
+ const tx = hex('11', 32);
25
+ const txv = hex('22', 32);
26
+ const nonce = hex('33', 32);
27
+ const amount = hex('44', 16);
28
+ const signature = hex('55', 64);
29
+
30
+ const payload = {
31
+ type: OperationType.TRANSFER,
32
+ address: sender,
33
+ tro: {
34
+ tx,
35
+ txv,
36
+ in: nonce,
37
+ to: recipient,
38
+ am: amount,
39
+ is: signature
40
+ }
41
+ };
42
+
43
+ const normalized = normalizeTransferOperation(payload, config);
44
+
45
+ t.is(normalized.type, OperationType.TRANSFER);
46
+ t.ok(b4a.equals(normalized.address, addressToBuffer(sender, config.addressPrefix)));
47
+ t.ok(b4a.equals(normalized.tro.tx, toBuffer(tx)));
48
+ t.ok(b4a.equals(normalized.tro.txv, toBuffer(txv)));
49
+ t.ok(b4a.equals(normalized.tro.in, toBuffer(nonce)));
50
+ t.ok(b4a.equals(normalized.tro.to, addressToBuffer(recipient, config.addressPrefix)));
51
+ t.ok(b4a.equals(normalized.tro.am, toBuffer(amount)));
52
+ t.ok(b4a.equals(normalized.tro.is, toBuffer(signature)));
53
+ });
54
+
55
+ test('normalizeTransferOperation accepts buffer inputs', t => {
56
+ const sender = randomAddress(config.addressPrefix);
57
+ const recipient = randomAddress(config.addressPrefix);
58
+ const tx = toBuffer(hex('aa', 32));
59
+ const txv = toBuffer(hex('bb', 32));
60
+ const nonce = toBuffer(hex('cc', 32));
61
+ const amount = toBuffer(hex('dd', 16));
62
+ const signature = toBuffer(hex('ee', 64));
63
+
64
+ const payload = {
65
+ type: OperationType.TRANSFER,
66
+ address: sender,
67
+ tro: {
68
+ tx,
69
+ txv,
70
+ in: nonce,
71
+ to: recipient,
72
+ am: amount,
73
+ is: signature
74
+ }
75
+ };
76
+
77
+ const normalized = normalizeTransferOperation(payload, config);
78
+ t.ok(b4a.equals(normalized.tro.tx, tx));
79
+ t.ok(b4a.equals(normalized.tro.txv, txv));
80
+ t.ok(b4a.equals(normalized.tro.in, nonce));
81
+ t.ok(b4a.equals(normalized.tro.am, amount));
82
+ t.ok(b4a.equals(normalized.tro.is, signature));
83
+ });
84
+
85
+ test('normalizeTransferOperation throws on missing payload fields', t => {
86
+ t.exception(
87
+ () => normalizeTransferOperation({ type: OperationType.TRANSFER }, config),
88
+ errorMessageIncludes('Invalid payload for transfer operation normalization.')
89
+ );
90
+
91
+ t.exception(
92
+ () => normalizeTransferOperation({ type: OperationType.TX, address: 'x', tro: {} }, config),
93
+ errorMessageIncludes('Missing required fields in transfer operation payload.')
94
+ );
95
+
96
+ const sender = randomAddress(config.addressPrefix);
97
+ const payload = {
98
+ type: OperationType.TRANSFER,
99
+ address: sender,
100
+ tro: {
101
+ tx: hex('11', 32),
102
+ txv: hex('22', 32),
103
+ in: hex('33', 32),
104
+ to: randomAddress(config.addressPrefix),
105
+ am: hex('44', 16)
106
+ }
107
+ };
108
+ t.exception(
109
+ () => normalizeTransferOperation(payload, config),
110
+ errorMessageIncludes('Missing required fields in transfer operation payload.')
111
+ );
112
+ });
113
+
114
+ test('normalizeTransferOperation throws on invalid hex string', t => {
115
+ const sender = randomAddress(config.addressPrefix);
116
+ const payload = {
117
+ type: OperationType.TRANSFER,
118
+ address: sender,
119
+ tro: {
120
+ tx: 'zz',
121
+ txv: hex('22', 32),
122
+ in: hex('33', 32),
123
+ to: randomAddress(config.addressPrefix),
124
+ am: hex('44', 16),
125
+ is: hex('55', 64)
126
+ }
127
+ };
128
+ t.exception(
129
+ () => normalizeTransferOperation(payload, config),
130
+ errorMessageIncludes('Invalid hex string')
131
+ );
132
+ });
133
+
134
+ test('normalizeTransactionOperation normalizes hex strings and addresses', t => {
135
+ const sender = randomAddress(config.addressPrefix);
136
+ const tx = hex('11', 32);
137
+ const txv = hex('22', 32);
138
+ const writerKey = hex('33', 32);
139
+ const contentHash = hex('44', 32);
140
+ const bootstrap = hex('55', 32);
141
+ const msbBootstrap = hex('66', 32);
142
+ const nonce = hex('77', 32);
143
+ const signature = hex('88', 64);
144
+
145
+ const payload = {
146
+ type: OperationType.TX,
147
+ address: sender,
148
+ txo: {
149
+ tx,
150
+ txv,
151
+ iw: writerKey,
152
+ ch: contentHash,
153
+ bs: bootstrap,
154
+ mbs: msbBootstrap,
155
+ in: nonce,
156
+ is: signature
157
+ }
158
+ };
159
+
160
+ const normalized = normalizeTransactionOperation(payload, config);
161
+ t.is(normalized.type, OperationType.TX);
162
+ t.ok(b4a.equals(normalized.address, addressToBuffer(sender, config.addressPrefix)));
163
+ t.ok(b4a.equals(normalized.txo.tx, toBuffer(tx)));
164
+ t.ok(b4a.equals(normalized.txo.txv, toBuffer(txv)));
165
+ t.ok(b4a.equals(normalized.txo.iw, toBuffer(writerKey)));
166
+ t.ok(b4a.equals(normalized.txo.ch, toBuffer(contentHash)));
167
+ t.ok(b4a.equals(normalized.txo.bs, toBuffer(bootstrap)));
168
+ t.ok(b4a.equals(normalized.txo.mbs, toBuffer(msbBootstrap)));
169
+ t.ok(b4a.equals(normalized.txo.in, toBuffer(nonce)));
170
+ t.ok(b4a.equals(normalized.txo.is, toBuffer(signature)));
171
+ });
172
+
173
+ test('normalizeTransactionOperation accepts buffer inputs', t => {
174
+ const sender = randomAddress(config.addressPrefix);
175
+ const tx = toBuffer(hex('aa', 32));
176
+ const txv = toBuffer(hex('bb', 32));
177
+ const writerKey = toBuffer(hex('cc', 32));
178
+ const contentHash = toBuffer(hex('dd', 32));
179
+ const bootstrap = toBuffer(hex('ee', 32));
180
+ const msbBootstrap = toBuffer(hex('ff', 32));
181
+ const nonce = toBuffer(hex('12', 32));
182
+ const signature = toBuffer(hex('34', 64));
183
+
184
+ const payload = {
185
+ type: OperationType.TX,
186
+ address: sender,
187
+ txo: {
188
+ tx,
189
+ txv,
190
+ iw: writerKey,
191
+ ch: contentHash,
192
+ bs: bootstrap,
193
+ mbs: msbBootstrap,
194
+ in: nonce,
195
+ is: signature
196
+ }
197
+ };
198
+
199
+ const normalized = normalizeTransactionOperation(payload, config);
200
+ t.ok(b4a.equals(normalized.txo.tx, tx));
201
+ t.ok(b4a.equals(normalized.txo.txv, txv));
202
+ t.ok(b4a.equals(normalized.txo.iw, writerKey));
203
+ t.ok(b4a.equals(normalized.txo.ch, contentHash));
204
+ t.ok(b4a.equals(normalized.txo.bs, bootstrap));
205
+ t.ok(b4a.equals(normalized.txo.mbs, msbBootstrap));
206
+ t.ok(b4a.equals(normalized.txo.in, nonce));
207
+ t.ok(b4a.equals(normalized.txo.is, signature));
208
+ });
209
+
210
+ test('normalizeTransactionOperation throws on missing payload fields', t => {
211
+ t.exception(
212
+ () => normalizeTransactionOperation({ type: OperationType.TX }, config),
213
+ errorMessageIncludes('Invalid payload for transaction operation normalization.')
214
+ );
215
+
216
+ t.exception(
217
+ () => normalizeTransactionOperation({ type: OperationType.TRANSFER, address: 'x', txo: {} }, config),
218
+ errorMessageIncludes('Missing required fields in transaction operation payload.')
219
+ );
220
+
221
+ const sender = randomAddress(config.addressPrefix);
222
+ const payload = {
223
+ type: OperationType.TX,
224
+ address: sender,
225
+ txo: {
226
+ tx: hex('11', 32),
227
+ txv: hex('22', 32),
228
+ iw: hex('33', 32),
229
+ ch: hex('44', 32),
230
+ bs: hex('55', 32),
231
+ mbs: hex('66', 32),
232
+ in: hex('77', 32)
233
+ }
234
+ };
235
+ t.exception(
236
+ () => normalizeTransactionOperation(payload, config),
237
+ errorMessageIncludes('Missing required fields in transaction operation payload.')
238
+ );
239
+ });
240
+
241
+ test('normalizeTransactionOperation throws on invalid hex string', t => {
242
+ const sender = randomAddress(config.addressPrefix);
243
+ const payload = {
244
+ type: OperationType.TX,
245
+ address: sender,
246
+ txo: {
247
+ tx: 'zz',
248
+ txv: hex('22', 32),
249
+ iw: hex('33', 32),
250
+ ch: hex('44', 32),
251
+ bs: hex('55', 32),
252
+ mbs: hex('66', 32),
253
+ in: hex('77', 32),
254
+ is: hex('88', 64)
255
+ }
256
+ };
257
+ t.exception(
258
+ () => normalizeTransactionOperation(payload, config),
259
+ errorMessageIncludes('Invalid hex string')
260
+ );
261
+ });
262
+
263
+ test('normalizeRoleAccessOperation normalizes hex strings and addresses', t => {
264
+ const sender = randomAddress(config.addressPrefix);
265
+ const payload = {
266
+ type: OperationType.ADD_WRITER,
267
+ address: sender,
268
+ rao: {
269
+ tx: hex('11', 32),
270
+ txv: hex('22', 32),
271
+ iw: hex('33', 32),
272
+ in: hex('44', 32),
273
+ is: hex('55', 64)
274
+ }
275
+ };
276
+
277
+ const normalized = normalizeRoleAccessOperation(payload, config);
278
+ t.is(normalized.type, OperationType.ADD_WRITER);
279
+ t.ok(b4a.equals(normalized.address, addressToBuffer(sender, config.addressPrefix)));
280
+ t.ok(b4a.equals(normalized.rao.tx, toBuffer(hex('11', 32))));
281
+ t.ok(b4a.equals(normalized.rao.txv, toBuffer(hex('22', 32))));
282
+ t.ok(b4a.equals(normalized.rao.iw, toBuffer(hex('33', 32))));
283
+ t.ok(b4a.equals(normalized.rao.in, toBuffer(hex('44', 32))));
284
+ t.ok(b4a.equals(normalized.rao.is, toBuffer(hex('55', 64))));
285
+ });
286
+
287
+ test('normalizeRoleAccessOperation accepts buffer inputs', t => {
288
+ const sender = randomAddress(config.addressPrefix);
289
+ const tx = toBuffer(hex('aa', 32));
290
+ const txv = toBuffer(hex('bb', 32));
291
+ const writerKey = toBuffer(hex('cc', 32));
292
+ const nonce = toBuffer(hex('dd', 32));
293
+ const signature = toBuffer(hex('ee', 64));
294
+
295
+ const payload = {
296
+ type: OperationType.REMOVE_WRITER,
297
+ address: sender,
298
+ rao: {
299
+ tx,
300
+ txv,
301
+ iw: writerKey,
302
+ in: nonce,
303
+ is: signature
304
+ }
305
+ };
306
+
307
+ const normalized = normalizeRoleAccessOperation(payload, config);
308
+ t.ok(b4a.equals(normalized.rao.tx, tx));
309
+ t.ok(b4a.equals(normalized.rao.txv, txv));
310
+ t.ok(b4a.equals(normalized.rao.iw, writerKey));
311
+ t.ok(b4a.equals(normalized.rao.in, nonce));
312
+ t.ok(b4a.equals(normalized.rao.is, signature));
313
+ });
314
+
315
+ test('normalizeRoleAccessOperation throws on missing payload fields', t => {
316
+ t.exception(
317
+ () => normalizeRoleAccessOperation({ type: OperationType.ADD_WRITER }, config),
318
+ errorMessageIncludes('Invalid payload for role access normalization.')
319
+ );
320
+
321
+ t.exception(
322
+ () => normalizeRoleAccessOperation({ type: OperationType.ADD_WRITER, address: 'x', rao: {} }, config),
323
+ errorMessageIncludes('Missing required fields in role access payload.')
324
+ );
325
+ });
326
+
327
+ test('normalizeRoleAccessOperation throws on invalid hex string', t => {
328
+ const sender = randomAddress(config.addressPrefix);
329
+ const payload = {
330
+ type: OperationType.ADD_WRITER,
331
+ address: sender,
332
+ rao: {
333
+ tx: 'zz',
334
+ txv: hex('22', 32),
335
+ iw: hex('33', 32),
336
+ in: hex('44', 32),
337
+ is: hex('55', 64)
338
+ }
339
+ };
340
+ t.exception(
341
+ () => normalizeRoleAccessOperation(payload, config),
342
+ errorMessageIncludes('Invalid hex string')
343
+ );
344
+ });
345
+
346
+ test('normalizeBootstrapDeploymentOperation normalizes hex strings and addresses', t => {
347
+ const sender = randomAddress(config.addressPrefix);
348
+ const payload = {
349
+ type: OperationType.BOOTSTRAP_DEPLOYMENT,
350
+ address: sender,
351
+ bdo: {
352
+ tx: hex('11', 32),
353
+ txv: hex('22', 32),
354
+ bs: hex('33', 32),
355
+ ic: hex('44', 32),
356
+ in: hex('55', 32),
357
+ is: hex('66', 64)
358
+ }
359
+ };
360
+
361
+ const normalized = normalizeBootstrapDeploymentOperation(payload, config);
362
+ t.is(normalized.type, OperationType.BOOTSTRAP_DEPLOYMENT);
363
+ t.ok(b4a.equals(normalized.address, addressToBuffer(sender, config.addressPrefix)));
364
+ t.ok(b4a.equals(normalized.bdo.tx, toBuffer(hex('11', 32))));
365
+ t.ok(b4a.equals(normalized.bdo.txv, toBuffer(hex('22', 32))));
366
+ t.ok(b4a.equals(normalized.bdo.bs, toBuffer(hex('33', 32))));
367
+ t.ok(b4a.equals(normalized.bdo.ic, toBuffer(hex('44', 32))));
368
+ t.ok(b4a.equals(normalized.bdo.in, toBuffer(hex('55', 32))));
369
+ t.ok(b4a.equals(normalized.bdo.is, toBuffer(hex('66', 64))));
370
+ });
371
+
372
+ test('normalizeBootstrapDeploymentOperation accepts buffer inputs', t => {
373
+ const sender = randomAddress(config.addressPrefix);
374
+ const tx = toBuffer(hex('aa', 32));
375
+ const txv = toBuffer(hex('bb', 32));
376
+ const bootstrap = toBuffer(hex('cc', 32));
377
+ const channel = toBuffer(hex('dd', 32));
378
+ const nonce = toBuffer(hex('ee', 32));
379
+ const signature = toBuffer(hex('ff', 64));
380
+
381
+ const payload = {
382
+ type: OperationType.BOOTSTRAP_DEPLOYMENT,
383
+ address: sender,
384
+ bdo: {
385
+ tx,
386
+ txv,
387
+ bs: bootstrap,
388
+ ic: channel,
389
+ in: nonce,
390
+ is: signature
391
+ }
392
+ };
393
+
394
+ const normalized = normalizeBootstrapDeploymentOperation(payload, config);
395
+ t.ok(b4a.equals(normalized.bdo.tx, tx));
396
+ t.ok(b4a.equals(normalized.bdo.txv, txv));
397
+ t.ok(b4a.equals(normalized.bdo.bs, bootstrap));
398
+ t.ok(b4a.equals(normalized.bdo.ic, channel));
399
+ t.ok(b4a.equals(normalized.bdo.in, nonce));
400
+ t.ok(b4a.equals(normalized.bdo.is, signature));
401
+ });
402
+
403
+ test('normalizeBootstrapDeploymentOperation throws on missing payload fields', t => {
404
+ t.exception(
405
+ () => normalizeBootstrapDeploymentOperation({ type: OperationType.BOOTSTRAP_DEPLOYMENT }, config),
406
+ errorMessageIncludes('Invalid payload for bootstrap deployment normalization.')
407
+ );
408
+
409
+ t.exception(
410
+ () => normalizeBootstrapDeploymentOperation({ type: OperationType.TX, address: 'x', bdo: {} }, config),
411
+ errorMessageIncludes('Missing required fields in bootstrap deployment payload.')
412
+ );
413
+ });
414
+
415
+ test('normalizeBootstrapDeploymentOperation throws on invalid hex string', t => {
416
+ const sender = randomAddress(config.addressPrefix);
417
+ const payload = {
418
+ type: OperationType.BOOTSTRAP_DEPLOYMENT,
419
+ address: sender,
420
+ bdo: {
421
+ tx: 'zz',
422
+ txv: hex('22', 32),
423
+ bs: hex('33', 32),
424
+ ic: hex('44', 32),
425
+ in: hex('55', 32),
426
+ is: hex('66', 64)
427
+ }
428
+ };
429
+ t.exception(
430
+ () => normalizeBootstrapDeploymentOperation(payload, config),
431
+ errorMessageIncludes('Invalid hex string')
432
+ );
433
+ });
434
+
435
+ test('normalizeDecodedPayloadForJson converts buffers to strings', t => {
436
+ const address = randomAddress(config.addressPrefix);
437
+ const addressBuf = addressToBuffer(address, config.addressPrefix);
438
+ const amountBuf = bigIntTo16ByteBuffer(1234n);
439
+ const otherBuf = b4a.from('abcd', 'hex');
440
+
441
+ const payload = {
442
+ address: addressBuf,
443
+ am: amountBuf,
444
+ nested: {
445
+ to: addressBuf,
446
+ amount: amountBuf,
447
+ other: otherBuf
448
+ }
449
+ };
450
+
451
+ const normalized = normalizeDecodedPayloadForJson(payload, config);
452
+ t.is(normalized.address, address);
453
+ t.is(normalized.am, '1234');
454
+ t.is(normalized.nested.to, address);
455
+ t.is(normalized.nested.amount, '1234');
456
+ t.is(normalized.nested.other, 'abcd');
457
+ });
458
+
459
+ test('normalizeDecodedPayloadForJson falls back to hex for invalid address buffers', t => {
460
+ const bad = b4a.from('deadbeef', 'hex');
461
+ const payload = { address: bad };
462
+ const normalized = normalizeDecodedPayloadForJson(payload, config);
463
+ t.is(normalized.address, b4a.toString(bad, 'hex'));
464
+ });
465
+
466
+ test('normalizeDecodedPayloadForJson returns input for non-objects', t => {
467
+ t.is(normalizeDecodedPayloadForJson(null, config), null);
468
+ t.is(normalizeDecodedPayloadForJson('value', config), 'value');
469
+ });