trac-msb 0.2.7 → 0.2.9
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.
- package/.github/workflows/publish.yml +8 -16
- package/msb.mjs +13 -25
- package/package.json +8 -4
- package/proto/network.proto +74 -0
- package/rpc/{create_server.mjs → create_server.js} +4 -4
- package/rpc/{handlers.mjs → handlers.js} +7 -7
- package/rpc/routes/{index.mjs → index.js} +1 -1
- package/rpc/routes/{v1.mjs → v1.js} +1 -1
- package/rpc/rpc_server.js +10 -0
- package/rpc/rpc_services.js +48 -7
- package/rpc/utils/{helpers.mjs → helpers.js} +1 -1
- package/src/config/config.js +137 -0
- package/src/config/env.js +63 -0
- package/src/core/network/Network.js +133 -119
- package/src/core/network/identity/NetworkWalletFactory.js +5 -6
- package/src/core/network/protocols/LegacyProtocol.js +67 -0
- package/src/core/network/protocols/NetworkMessages.js +48 -0
- package/src/core/network/protocols/ProtocolInterface.js +31 -0
- package/src/core/network/protocols/ProtocolSession.js +59 -0
- package/src/core/network/protocols/V1Protocol.js +64 -0
- package/src/core/network/protocols/legacy/NetworkMessageRouter.js +84 -0
- package/src/core/network/protocols/legacy/handlers/GetRequestHandler.js +53 -0
- package/src/core/network/protocols/legacy/handlers/ResponseHandler.js +37 -0
- package/src/core/network/{messaging → protocols/legacy}/validators/ValidatorResponse.js +2 -2
- package/src/core/network/{messaging → protocols/legacy}/validators/base/BaseResponse.js +13 -6
- package/src/core/network/protocols/shared/handlers/RoleOperationHandler.js +88 -0
- package/src/core/network/protocols/shared/handlers/SubnetworkOperationHandler.js +93 -0
- package/src/core/network/protocols/shared/handlers/TransferOperationHandler.js +57 -0
- package/src/core/network/{messaging → protocols/shared}/handlers/base/BaseOperationHandler.js +21 -26
- package/src/core/network/{messaging → protocols/shared}/validators/PartialBootstrapDeployment.js +3 -3
- package/src/core/network/{messaging → protocols/shared}/validators/PartialRoleAccess.js +15 -12
- package/src/core/network/{messaging → protocols/shared}/validators/PartialTransaction.js +10 -11
- package/src/core/network/{messaging → protocols/shared}/validators/PartialTransfer.js +10 -7
- package/src/core/network/{messaging → protocols/shared}/validators/base/PartialOperation.js +40 -22
- package/src/core/network/protocols/v1/NetworkMessageRouter.js +15 -0
- package/src/core/network/services/ConnectionManager.js +13 -19
- package/src/core/network/services/MessageOrchestrator.js +10 -22
- package/src/core/network/services/TransactionPoolService.js +10 -10
- package/src/core/network/services/TransactionRateLimiterService.js +5 -3
- package/src/core/network/services/ValidatorObserverService.js +46 -21
- package/src/core/state/State.js +137 -141
- package/src/core/state/utils/address.js +18 -16
- package/src/core/state/utils/adminEntry.js +17 -16
- package/src/core/state/utils/deploymentEntry.js +15 -15
- package/src/core/state/utils/transaction.js +3 -95
- package/src/index.js +250 -325
- package/src/messages/network/v1/NetworkMessageBuilder.js +325 -0
- package/src/messages/network/v1/NetworkMessageDirector.js +137 -0
- package/src/messages/network/v1/networkMessageFactory.js +12 -0
- package/src/messages/state/ApplyStateMessageBuilder.js +661 -0
- package/src/messages/state/ApplyStateMessageDirector.js +516 -0
- package/src/messages/state/applyStateMessageFactory.js +12 -0
- package/src/utils/buffer.js +53 -1
- package/src/utils/check.js +21 -17
- package/src/utils/cli.js +0 -8
- package/src/utils/cliCommands.js +11 -11
- package/src/utils/constants.js +36 -24
- package/src/utils/fileUtils.js +1 -4
- package/src/utils/helpers.js +9 -20
- package/src/utils/migrationUtils.js +2 -2
- package/src/utils/normalizers.js +94 -11
- package/src/utils/protobuf/network.cjs +840 -0
- package/src/utils/protobuf/operationHelpers.js +10 -0
- package/tests/acceptance/v1/account/account.test.mjs +2 -2
- package/tests/acceptance/v1/balance/balance.test.mjs +1 -1
- package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +11 -2
- package/tests/acceptance/v1/rpc.test.mjs +10 -10
- package/tests/acceptance/v1/tx/tx.test.mjs +4 -2
- package/tests/acceptance/v1/tx-details/tx-details.test.mjs +7 -3
- package/tests/fixtures/check.fixtures.js +42 -42
- package/tests/fixtures/networkV1.fixtures.js +84 -0
- package/tests/fixtures/protobuf.fixtures.js +110 -26
- package/tests/helpers/StateNetworkFactory.js +3 -5
- package/tests/helpers/autobaseTestHelpers.js +1 -2
- package/tests/helpers/config.js +3 -0
- package/tests/helpers/setupApplyTests.js +113 -99
- package/tests/helpers/transactionPayloads.mjs +26 -12
- package/tests/unit/messages/messages.test.js +12 -0
- package/tests/unit/messages/network/NetworkMessageBuilder.test.js +276 -0
- package/tests/unit/messages/network/NetworkMessageDirector.test.js +203 -0
- package/tests/unit/messages/state/applyStateMessageBuilder.complete.test.js +521 -0
- package/tests/unit/messages/state/applyStateMessageBuilder.partial.test.js +233 -0
- package/tests/unit/network/ConnectionManager.test.js +10 -7
- package/tests/unit/network/NetworkWalletFactory.test.js +14 -14
- package/tests/unit/network/networkModule.test.js +3 -2
- package/tests/unit/state/apply/addAdmin/addAdminHappyPathScenario.js +10 -6
- package/tests/unit/state/apply/addAdmin/addAdminScenarioHelpers.js +11 -8
- package/tests/unit/state/apply/addAdmin/state.apply.addAdmin.test.js +11 -7
- package/tests/unit/state/apply/addIndexer/addIndexerScenarioHelpers.js +18 -20
- package/tests/unit/state/apply/addWriter/addWriterScenarioHelpers.js +57 -48
- package/tests/unit/state/apply/addWriter/addWriterValidatorRewardScenario.js +2 -1
- package/tests/unit/state/apply/adminRecovery/adminRecoveryScenarioHelpers.js +72 -57
- package/tests/unit/state/apply/adminRecovery/state.apply.adminRecovery.test.js +3 -7
- package/tests/unit/state/apply/appendWhitelist/appendWhitelistScenarioHelpers.js +12 -14
- package/tests/unit/state/apply/balanceInitialization/balanceInitializationScenarioHelpers.js +18 -13
- package/tests/unit/state/apply/balanceInitialization/nodeEntryBalanceUpdateFailureScenario.js +2 -1
- package/tests/unit/state/apply/banValidator/banValidatorBanAndReWhitelistScenario.js +2 -1
- package/tests/unit/state/apply/banValidator/banValidatorScenarioHelpers.js +27 -30
- package/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentDuplicateRegistrationScenario.js +2 -1
- package/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentScenarioHelpers.js +24 -21
- package/tests/unit/state/apply/common/access-control/adminConsistencyMismatchScenario.js +5 -4
- package/tests/unit/state/apply/common/access-control/adminPublicKeyDecodeFailureScenario.js +4 -3
- package/tests/unit/state/apply/common/balances/base/requesterBalanceScenarioBase.js +2 -1
- package/tests/unit/state/apply/common/commonScenarioHelper.js +16 -16
- package/tests/unit/state/apply/common/payload-structure/initializationDisabledScenario.js +10 -5
- package/tests/unit/state/apply/common/payload-structure/invalidHashValidationScenario.js +2 -2
- package/tests/unit/state/apply/common/requester/requesterNodeEntryBufferMissingScenario.js +2 -1
- package/tests/unit/state/apply/common/requester/requesterNodeEntryDecodeFailureScenario.js +2 -1
- package/tests/unit/state/apply/common/validatorConsistency/base/validatorConsistencyScenarioBase.js +2 -1
- package/tests/unit/state/apply/common/validatorEntryValidation/base/validatorEntryValidationScenarioBase.js +2 -1
- package/tests/unit/state/apply/disableInitialization/disableInitializationScenarioHelpers.js +16 -9
- package/tests/unit/state/apply/removeIndexer/removeIndexerScenarioHelpers.js +6 -5
- package/tests/unit/state/apply/removeWriter/removeWriterScenarioHelpers.js +23 -19
- package/tests/unit/state/apply/transfer/transferDoubleSpendAcrossValidatorsScenario.js +45 -36
- package/tests/unit/state/apply/transfer/transferScenarioHelpers.js +48 -45
- package/tests/unit/state/apply/txOperation/txOperationScenarioHelpers.js +32 -29
- package/tests/unit/state/apply/txOperation/txOperationTransferFeeGuardScenarioFactory.js +2 -1
- package/tests/unit/state/stateModule.test.js +0 -1
- package/tests/unit/state/stateTestUtils.js +7 -3
- package/tests/unit/state/utils/address.test.js +3 -3
- package/tests/unit/state/utils/adminEntry.test.js +10 -9
- package/tests/unit/unit.test.js +1 -1
- package/tests/unit/utils/buffer/buffer.test.js +62 -1
- package/tests/unit/utils/check/adminControlOperation.test.js +3 -3
- package/tests/unit/utils/check/balanceInitializationOperation.test.js +2 -2
- package/tests/unit/utils/check/bootstrapDeploymentOperation.test.js +2 -3
- package/tests/unit/utils/check/common.test.js +7 -6
- package/tests/unit/utils/check/coreAdminOperation.test.js +3 -3
- package/tests/unit/utils/check/roleAccessOperation.test.js +3 -2
- package/tests/unit/utils/check/transactionOperation.test.js +3 -3
- package/tests/unit/utils/check/transferOperation.test.js +3 -3
- package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +2 -1
- package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +2 -1
- package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +7 -0
- package/tests/unit/utils/normalizers/normalizers.test.js +469 -0
- package/tests/unit/utils/protobuf/operationHelpers.test.js +120 -2
- package/tests/unit/utils/utils.test.js +0 -1
- package/rpc/rpc_server.mjs +0 -10
- package/src/core/network/messaging/NetworkMessages.js +0 -63
- package/src/core/network/messaging/handlers/GetRequestHandler.js +0 -112
- package/src/core/network/messaging/handlers/ResponseHandler.js +0 -108
- package/src/core/network/messaging/handlers/RoleOperationHandler.js +0 -116
- package/src/core/network/messaging/handlers/SubnetworkOperationHandler.js +0 -143
- package/src/core/network/messaging/handlers/TransferOperationHandler.js +0 -52
- package/src/core/network/messaging/routes/NetworkMessageRouter.js +0 -94
- package/src/core/network/messaging/validators/AdminResponse.js +0 -58
- package/src/core/network/messaging/validators/CustomNodeResponse.js +0 -46
- package/src/core/state/utils/indexerEntry.js +0 -105
- package/src/messages/base/StateBuilder.js +0 -25
- package/src/messages/completeStateMessages/CompleteStateMessageBuilder.js +0 -421
- package/src/messages/completeStateMessages/CompleteStateMessageDirector.js +0 -252
- package/src/messages/completeStateMessages/CompleteStateMessageOperations.js +0 -299
- package/src/messages/partialStateMessages/PartialStateMessageBuilder.js +0 -272
- package/src/messages/partialStateMessages/PartialStateMessageDirector.js +0 -137
- package/src/messages/partialStateMessages/PartialStateMessageOperations.js +0 -131
- package/src/utils/crypto.js +0 -11
- package/tests/integration/apply/addAdmin/addAdminBasic.test.js +0 -68
- package/tests/integration/apply/addAdmin/addAdminRecovery.test.js +0 -125
- package/tests/integration/apply/addIndexer.test.js +0 -237
- package/tests/integration/apply/addWhitelist.test.js +0 -53
- package/tests/integration/apply/addWriter.test.js +0 -244
- package/tests/integration/apply/apply.test.js +0 -19
- package/tests/integration/apply/banValidator.test.js +0 -109
- package/tests/integration/apply/postTx/invalidSubValues.test.js +0 -103
- package/tests/integration/apply/postTx/postTx.test.js +0 -222
- package/tests/integration/apply/removeIndexer.test.js +0 -128
- package/tests/integration/apply/removeWriter.test.js +0 -167
- package/tests/integration/apply/transfer.test.js +0 -81
- package/tests/integration/integration.test.js +0 -9
- package/tests/unit/messageOperations/assembleAddIndexerMessage.test.js +0 -21
- package/tests/unit/messageOperations/assembleAddWriterMessage.test.js +0 -16
- package/tests/unit/messageOperations/assembleAdminMessage.test.js +0 -69
- package/tests/unit/messageOperations/assembleBanWriterMessage.test.js +0 -16
- package/tests/unit/messageOperations/assemblePostTransaction.test.js +0 -442
- package/tests/unit/messageOperations/assembleRemoveIndexerMessage.test.js +0 -19
- package/tests/unit/messageOperations/assembleRemoveWriterMessage.test.js +0 -17
- package/tests/unit/messageOperations/assembleWhitelistMessages.test.js +0 -58
- package/tests/unit/messageOperations/commonsStateMessageOperationsTest.js +0 -277
- package/tests/unit/messageOperations/stateMessageOperations.test.js +0 -19
- package/tests/unit/state/utils/indexerEntry.test.js +0 -83
- package/tests/unit/state/utils/transaction.test.js +0 -97
- package/tests/unit/utils/crypto/createHash.test.js +0 -15
- /package/rpc/{constants.mjs → constants.js} +0 -0
- /package/rpc/{cors.mjs → cors.js} +0 -0
- /package/rpc/utils/{confirmedParameter.mjs → confirmedParameter.js} +0 -0
- /package/rpc/utils/{url.mjs → url.js} +0 -0
- /package/src/utils/{operations.js → applyOperations.js} +0 -0
|
@@ -5,37 +5,29 @@ on:
|
|
|
5
5
|
types: [published]
|
|
6
6
|
|
|
7
7
|
permissions:
|
|
8
|
+
id-token: write # Required for OIDC
|
|
8
9
|
contents: read
|
|
9
10
|
|
|
10
11
|
jobs:
|
|
11
|
-
publish
|
|
12
|
+
publish:
|
|
12
13
|
runs-on: ubuntu-latest
|
|
13
|
-
|
|
14
14
|
steps:
|
|
15
|
-
- name: Checkout
|
|
15
|
+
- name: Checkout release tag
|
|
16
16
|
uses: actions/checkout@v4
|
|
17
|
+
with:
|
|
18
|
+
ref: ${{ github.event.release.tag_name }}
|
|
17
19
|
|
|
18
20
|
- name: Use Node.js
|
|
19
21
|
uses: actions/setup-node@v4
|
|
20
22
|
with:
|
|
21
23
|
node-version: '24'
|
|
22
24
|
registry-url: 'https://registry.npmjs.org'
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: npm ci
|
|
23
27
|
|
|
24
|
-
- name: Set package.json version from tag
|
|
25
|
-
run: |
|
|
26
|
-
TAG="${GITHUB_REF#refs/tags/}"
|
|
27
|
-
VERSION="${TAG#v}"
|
|
28
|
-
echo "Version from tag: $VERSION"
|
|
29
|
-
npm version "$VERSION" --no-git-tag-version
|
|
30
|
-
|
|
31
28
|
# unit tests are temporarily disabled because they lost stability on GH runners.
|
|
32
|
-
#- name: Install dependencies
|
|
33
|
-
# run: npm ci
|
|
34
|
-
|
|
35
29
|
#- name: Run unit tests
|
|
36
30
|
# run: npm run test:unit:all
|
|
37
31
|
|
|
38
32
|
- name: Publish to npm
|
|
39
|
-
|
|
40
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_MSB_PUBLISH_TOKEN }}
|
|
41
|
-
run: npm publish
|
|
33
|
+
run: npm publish --access public
|
package/msb.mjs
CHANGED
|
@@ -1,33 +1,22 @@
|
|
|
1
1
|
import { MainSettlementBus } from './src/index.js';
|
|
2
|
+
import { startRpcServer } from './rpc/rpc_server.js';
|
|
3
|
+
import { createConfig, ENV } from './src/config/env.js';
|
|
2
4
|
|
|
3
5
|
const pearApp = typeof Pear !== 'undefined' ? (Pear.app ?? Pear.config) : undefined;
|
|
4
6
|
const runtimeArgs = typeof process !== 'undefined' ? process.argv.slice(2) : [];
|
|
5
7
|
const args = pearApp?.args ?? runtimeArgs;
|
|
6
8
|
const runRpc = args.includes('--rpc');
|
|
9
|
+
const storeName = pearApp?.args?.[0] ?? runtimeArgs[0]
|
|
7
10
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
enable_role_requester: false,
|
|
14
|
-
enable_wallet: true,
|
|
15
|
-
enable_validator_observer: true,
|
|
16
|
-
enable_interactive_mode: true,
|
|
17
|
-
disable_rate_limit: false,
|
|
18
|
-
enable_tx_apply_logs: false,
|
|
19
|
-
enable_error_apply_logs: false,
|
|
20
|
-
};
|
|
11
|
+
const rpc = {
|
|
12
|
+
storeName: pearApp?.args?.[0] ?? runtimeArgs[0],
|
|
13
|
+
enableWallet: false,
|
|
14
|
+
enableInteractiveMode: false
|
|
15
|
+
}
|
|
21
16
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
enable_error_apply_logs: false,
|
|
26
|
-
enable_wallet: false,
|
|
27
|
-
enable_interactive_mode: false,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const msb = new MainSettlementBus(runRpc ? rpc_opts : opts);
|
|
17
|
+
const options = args.includes('--rpc') ? rpc : { storeName }
|
|
18
|
+
const config = createConfig(ENV.MAINNET, options)
|
|
19
|
+
const msb = new MainSettlementBus(config);
|
|
31
20
|
|
|
32
21
|
msb.ready().then(async () => {
|
|
33
22
|
if (runRpc) {
|
|
@@ -36,10 +25,9 @@ msb.ready().then(async () => {
|
|
|
36
25
|
const port = (portIndex !== -1 && args[portIndex + 1]) ? parseInt(args[portIndex + 1], 10) : 5000;
|
|
37
26
|
const hostIndex = args.indexOf('--host');
|
|
38
27
|
const host = (hostIndex !== -1 && args[hostIndex + 1]) ? args[hostIndex + 1] : 'localhost';
|
|
39
|
-
|
|
40
|
-
const {startRpcServer} = await import('./rpc/rpc_server.mjs');
|
|
41
|
-
startRpcServer(msb, host, port);
|
|
28
|
+
startRpcServer(msb, config , host, port);
|
|
42
29
|
} else {
|
|
30
|
+
console.log('RPC server will not be started.');
|
|
43
31
|
msb.interactiveMode();
|
|
44
32
|
}
|
|
45
33
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trac-msb",
|
|
3
3
|
"main": "msb.mjs",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.9",
|
|
5
5
|
"pear": {
|
|
6
6
|
"name": "trac-msb",
|
|
7
7
|
"type": "terminal"
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"test:unit:bare:cov": "brittle-bare -c -t 60000 tests/unit/unit.test.js"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@tracsystems/blake3": "0.0.13",
|
|
29
28
|
"autobase": "7.20.1",
|
|
30
29
|
"b4a": "1.6.7",
|
|
31
30
|
"bare-crypto": "1.12.0",
|
|
@@ -50,8 +49,9 @@
|
|
|
50
49
|
"protomux-wakeup": "2.4.0",
|
|
51
50
|
"readline": "npm:bare-node-readline",
|
|
52
51
|
"ready-resource": "1.1.2",
|
|
53
|
-
"trac-wallet": "
|
|
54
|
-
"tty": "npm:bare-node-tty"
|
|
52
|
+
"trac-wallet": "1.0.1",
|
|
53
|
+
"tty": "npm:bare-node-tty",
|
|
54
|
+
"uuid": "^13.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"bare-os": "^3.6.1",
|
|
@@ -67,5 +67,9 @@
|
|
|
67
67
|
"publishConfig": {
|
|
68
68
|
"registry": "https://registry.npmjs.org",
|
|
69
69
|
"access": "public"
|
|
70
|
+
},
|
|
71
|
+
"repository": {
|
|
72
|
+
"type": "git",
|
|
73
|
+
"url": "https://github.com/Trac-Systems/main_settlement_bus"
|
|
70
74
|
}
|
|
71
75
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package network.v1;
|
|
4
|
+
|
|
5
|
+
enum MessageType {
|
|
6
|
+
MESSAGE_TYPE_UNSPECIFIED = 0;
|
|
7
|
+
MESSAGE_TYPE_VALIDATOR_CONNECTION_REQUEST = 1;
|
|
8
|
+
MESSAGE_TYPE_VALIDATOR_CONNECTION_RESPONSE = 2;
|
|
9
|
+
MESSAGE_TYPE_LIVENESS_REQUEST = 3;
|
|
10
|
+
MESSAGE_TYPE_LIVENESS_RESPONSE = 4;
|
|
11
|
+
MESSAGE_TYPE_BROADCAST_TRANSACTION_REQUEST = 5;
|
|
12
|
+
MESSAGE_TYPE_BROADCAST_TRANSACTION_RESPONSE = 6;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
enum ResultCode {
|
|
16
|
+
RESULT_CODE_UNSPECIFIED = 0;
|
|
17
|
+
RESULT_CODE_OK = 1;
|
|
18
|
+
RESULT_CODE_INVALID_PAYLOAD = 2;
|
|
19
|
+
RESULT_CODE_UNSUPPORTED_VERSION = 3;
|
|
20
|
+
RESULT_CODE_RATE_LIMITED = 4;
|
|
21
|
+
RESULT_CODE_TIMEOUT = 5;
|
|
22
|
+
RESULT_CODE_SIGNATURE_INVALID = 6;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
message ValidatorConnectionRequest {
|
|
26
|
+
string issuer_address = 1;
|
|
27
|
+
bytes nonce = 2;
|
|
28
|
+
bytes signature = 3;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
message ValidatorConnectionResponse {
|
|
32
|
+
string issuer_address = 1;
|
|
33
|
+
bytes nonce = 2;
|
|
34
|
+
bytes signature = 3;
|
|
35
|
+
ResultCode result = 4;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
message LivenessRequest {
|
|
39
|
+
bytes nonce = 1;
|
|
40
|
+
bytes signature = 2;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
message LivenessResponse {
|
|
44
|
+
bytes nonce = 1;
|
|
45
|
+
bytes signature = 2;
|
|
46
|
+
ResultCode result = 3;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
message BroadcastTransactionRequest {
|
|
50
|
+
bytes data = 1; // binary encoded payload
|
|
51
|
+
bytes nonce = 2;
|
|
52
|
+
bytes signature = 3;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
message BroadcastTransactionResponse {
|
|
56
|
+
bytes nonce = 1;
|
|
57
|
+
bytes signature = 2;
|
|
58
|
+
ResultCode result = 3;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
message MessageHeader {
|
|
62
|
+
MessageType type = 1;
|
|
63
|
+
string id = 2;
|
|
64
|
+
uint64 timestamp = 3;
|
|
65
|
+
oneof field {
|
|
66
|
+
ValidatorConnectionRequest validator_connection_request = 4;
|
|
67
|
+
ValidatorConnectionResponse validator_connection_response = 5;
|
|
68
|
+
LivenessRequest liveness_request = 6;
|
|
69
|
+
LivenessResponse liveness_response = 7;
|
|
70
|
+
BroadcastTransactionRequest broadcast_transaction_request = 8;
|
|
71
|
+
BroadcastTransactionResponse broadcast_transaction_response = 9;
|
|
72
|
+
}
|
|
73
|
+
repeated string capabilities = 10;
|
|
74
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// rpc_server.mjs
|
|
2
2
|
import http from 'http'
|
|
3
|
-
import { applyCors } from './cors.
|
|
4
|
-
import { routes } from './routes/index.
|
|
3
|
+
import { applyCors } from './cors.js';
|
|
4
|
+
import { routes } from './routes/index.js';
|
|
5
5
|
|
|
6
|
-
export const createServer = (msbInstance) => {
|
|
6
|
+
export const createServer = (msbInstance, config) => {
|
|
7
7
|
const server = http.createServer({}, async (req, res) => {
|
|
8
8
|
|
|
9
9
|
// --- 1. Define safe 'respond' utility (Payload MUST be an object) ---
|
|
@@ -53,7 +53,7 @@ export const createServer = (msbInstance) => {
|
|
|
53
53
|
try {
|
|
54
54
|
// This try/catch covers synchronous errors and errors from awaited promises
|
|
55
55
|
// within the route.handler function.
|
|
56
|
-
await route.handler({ req, res, respond,
|
|
56
|
+
await route.handler({ req, res, respond, msbInstance, config});
|
|
57
57
|
} catch (error) {
|
|
58
58
|
// Catch errors thrown directly from the handler (or its awaited parts)
|
|
59
59
|
console.error(`Error on ${route.path}:`, error);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { decodeBase64Payload, isBase64, sanitizeBulkPayloadsRequestBody, sanitizeTransferPayload, validatePayloadStructure } from "./utils/helpers.
|
|
2
|
-
import { MAX_SIGNED_LENGTH, ZERO_WK } from "./constants.
|
|
3
|
-
import { buildRequestUrl } from "./utils/url.
|
|
1
|
+
import { decodeBase64Payload, isBase64, sanitizeBulkPayloadsRequestBody, sanitizeTransferPayload, validatePayloadStructure } from "./utils/helpers.js"
|
|
2
|
+
import { MAX_SIGNED_LENGTH, ZERO_WK } from "./constants.js";
|
|
3
|
+
import { buildRequestUrl } from "./utils/url.js";
|
|
4
4
|
import { isHexString } from "../src/utils/helpers.js";
|
|
5
5
|
import {
|
|
6
6
|
getBalance,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "./rpc_services.js";
|
|
17
17
|
import { bufferToBigInt, licenseBufferToBigInt } from "../src/utils/amountSerialization.js";
|
|
18
18
|
import { isAddressValid } from "../src/core/state/utils/address.js";
|
|
19
|
-
import { getConfirmedParameter } from "./utils/confirmedParameter.
|
|
19
|
+
import { getConfirmedParameter } from "./utils/confirmedParameter.js";
|
|
20
20
|
|
|
21
21
|
export async function handleBalance({ req, respond, msbInstance }) {
|
|
22
22
|
const url = buildRequestUrl(req);
|
|
@@ -52,7 +52,7 @@ export async function handleConfirmedLength({ msbInstance, respond }) {
|
|
|
52
52
|
respond(200, { confirmed_length });
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export async function handleBroadcastTransaction({ msbInstance, respond, req }) {
|
|
55
|
+
export async function handleBroadcastTransaction({ msbInstance, config, respond, req }) {
|
|
56
56
|
let body = '';
|
|
57
57
|
req.on('data', chunk => {
|
|
58
58
|
body += chunk.toString();
|
|
@@ -72,7 +72,7 @@ export async function handleBroadcastTransaction({ msbInstance, respond, req })
|
|
|
72
72
|
const decodedPayload = decodeBase64Payload(payload);
|
|
73
73
|
validatePayloadStructure(decodedPayload);
|
|
74
74
|
const sanitizedPayload = sanitizeTransferPayload(decodedPayload);
|
|
75
|
-
const result = await broadcastTransaction(msbInstance, sanitizedPayload);
|
|
75
|
+
const result = await broadcastTransaction(msbInstance, config, sanitizedPayload);
|
|
76
76
|
respond(200, { result });
|
|
77
77
|
} catch (error) {
|
|
78
78
|
let code = error instanceof SyntaxError ? 400 : 500;
|
|
@@ -258,7 +258,7 @@ export async function handleAccountDetails({ msbInstance, respond, req }) {
|
|
|
258
258
|
return respond(400, { error: 'Parameter "confirmed" must be exactly "true" or "false"' });
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
if (!isAddressValid(address)) {
|
|
261
|
+
if (!isAddressValid(address, msbInstance.config.addressPrefix)) {
|
|
262
262
|
return respond(400, { error: "Invalid account address format" });
|
|
263
263
|
}
|
|
264
264
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createServer } from "./create_server.js";
|
|
2
|
+
|
|
3
|
+
// Called by msb.mjs file
|
|
4
|
+
export function startRpcServer(msbInstance, config ,host, port) {
|
|
5
|
+
const server = createServer(msbInstance, config)
|
|
6
|
+
|
|
7
|
+
return server.listen(port, host, () => {
|
|
8
|
+
console.log(`Running RPC with http at http://${host}:${port}`);
|
|
9
|
+
});
|
|
10
|
+
}
|
package/rpc/rpc_services.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { bufferToBigInt } from "../src/utils/amountSerialization.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
normalizeDecodedPayloadForJson,
|
|
4
|
+
normalizeTransactionOperation,
|
|
5
|
+
normalizeTransferOperation
|
|
6
|
+
} from "../src/utils/normalizers.js";
|
|
3
7
|
import { get_confirmed_tx_info, get_unconfirmed_tx_info } from "../src/utils/cli.js";
|
|
8
|
+
import {OperationType} from "../src/utils/constants.js";
|
|
9
|
+
import b4a from "b4a";
|
|
10
|
+
import PartialTransaction from "../src/core/network/protocols/shared/validators/PartialTransaction.js";
|
|
11
|
+
import PartialTransfer from "../src/core/network/protocols/shared/validators/PartialTransfer.js";
|
|
4
12
|
|
|
5
13
|
export async function getBalance(msbInstance, address, confirmed) {
|
|
6
14
|
const state = msbInstance.state;
|
|
@@ -36,8 +44,41 @@ export async function getUnconfirmedLength(msbInstance) {
|
|
|
36
44
|
return msbInstance.state.getUnsignedLength();
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
export async function broadcastTransaction(msbInstance, payload) {
|
|
40
|
-
|
|
47
|
+
export async function broadcastTransaction(msbInstance, config, payload) {
|
|
48
|
+
if (!payload) {
|
|
49
|
+
throw new Error("Transaction payload is required for broadcasting.");
|
|
50
|
+
}
|
|
51
|
+
let normalizedPayload;
|
|
52
|
+
let isValid = false;
|
|
53
|
+
let hash;
|
|
54
|
+
|
|
55
|
+
const partialTransferValidator = new PartialTransfer(msbInstance.state, null , config);
|
|
56
|
+
const partialTransactionValidator = new PartialTransaction(msbInstance.state, null , config);
|
|
57
|
+
|
|
58
|
+
if (payload.type === OperationType.TRANSFER) {
|
|
59
|
+
normalizedPayload = normalizeTransferOperation(payload, config);
|
|
60
|
+
isValid = await partialTransferValidator.validate(normalizedPayload);
|
|
61
|
+
hash = b4a.toString(normalizedPayload.tro.tx, "hex");
|
|
62
|
+
} else if (payload.type === OperationType.TX) {
|
|
63
|
+
normalizedPayload = normalizeTransactionOperation(payload, config);
|
|
64
|
+
isValid = await partialTransactionValidator.validate(normalizedPayload);
|
|
65
|
+
hash = b4a.toString(normalizedPayload.txo.tx, "hex");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!isValid) {
|
|
69
|
+
throw new Error("Invalid transaction payload.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const success = await msbInstance.broadcastPartialTransaction(payload);
|
|
73
|
+
|
|
74
|
+
if (!success) {
|
|
75
|
+
throw new Error("Failed to broadcast transaction after multiple attempts.");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const signedLength = msbInstance.state.getSignedLength();
|
|
79
|
+
const unsignedLength = msbInstance.state.getUnsignedLength();
|
|
80
|
+
|
|
81
|
+
return { message: "Transaction broadcasted successfully.", signedLength, unsignedLength, tx: hash };
|
|
41
82
|
}
|
|
42
83
|
|
|
43
84
|
export async function getTxHashes(msbInstance, start, end) {
|
|
@@ -51,7 +92,7 @@ export async function getTxDetails(msbInstance, hash) {
|
|
|
51
92
|
return null;
|
|
52
93
|
}
|
|
53
94
|
|
|
54
|
-
return normalizeDecodedPayloadForJson(rawPayload.decoded);
|
|
95
|
+
return normalizeDecodedPayloadForJson(rawPayload.decoded, msbInstance.config);
|
|
55
96
|
}
|
|
56
97
|
|
|
57
98
|
export async function fetchBulkTxPayloads(msbInstance, hashes) {
|
|
@@ -73,7 +114,7 @@ export async function fetchBulkTxPayloads(msbInstance, hashes) {
|
|
|
73
114
|
if (result === null || result === undefined) {
|
|
74
115
|
res.missing.push(hash);
|
|
75
116
|
} else {
|
|
76
|
-
const decodedResult = normalizeDecodedPayloadForJson(result.decoded);
|
|
117
|
+
const decodedResult = normalizeDecodedPayloadForJson(result.decoded, msbInstance.config);
|
|
77
118
|
res.results.push({ hash, payload: decodedResult });
|
|
78
119
|
}
|
|
79
120
|
});
|
|
@@ -93,7 +134,7 @@ export async function getExtendedTxDetails(msbInstance, hash, confirmed) {
|
|
|
93
134
|
if (confirmedLength === null) {
|
|
94
135
|
throw new Error(`No confirmed length found for tx hash: ${hash} in confirmed mode`);
|
|
95
136
|
}
|
|
96
|
-
const normalizedPayload = normalizeDecodedPayloadForJson(rawPayload.decoded,
|
|
137
|
+
const normalizedPayload = normalizeDecodedPayloadForJson(rawPayload.decoded, msbInstance.config);
|
|
97
138
|
const feeBuffer = state.getFee();
|
|
98
139
|
return {
|
|
99
140
|
txDetails: normalizedPayload,
|
|
@@ -107,7 +148,7 @@ export async function getExtendedTxDetails(msbInstance, hash, confirmed) {
|
|
|
107
148
|
throw new Error(`No payload found for tx hash: ${hash}`);
|
|
108
149
|
}
|
|
109
150
|
|
|
110
|
-
const normalizedPayload = normalizeDecodedPayloadForJson(rawPayload.decoded,
|
|
151
|
+
const normalizedPayload = normalizeDecodedPayloadForJson(rawPayload.decoded, msbInstance.config);
|
|
111
152
|
const length = await state.getTransactionConfirmedLength(hash);
|
|
112
153
|
if (length === null) {
|
|
113
154
|
return {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import b4a from 'b4a'
|
|
2
|
+
|
|
3
|
+
export class Config {
|
|
4
|
+
#options
|
|
5
|
+
#config
|
|
6
|
+
#bootstrap
|
|
7
|
+
#channel
|
|
8
|
+
|
|
9
|
+
constructor(options = {}, config = {}) {
|
|
10
|
+
this.#validate(options, config)
|
|
11
|
+
this.#options = options
|
|
12
|
+
this.#config = config
|
|
13
|
+
this.#bootstrap = b4a.from(this.#options.bootstrap || this.#config.bootstrap, 'hex')
|
|
14
|
+
// Ensure a 32-byte channel buffer (repeat-fill from string/Buffer if provided)
|
|
15
|
+
this.#channel = b4a.alloc(32).fill(this.#options.channel || this.#config.channel)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get addressLength() {
|
|
19
|
+
return this.#config.addressLength
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get addressPrefix() {
|
|
23
|
+
return this.#config.addressPrefix
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get addressPrefixLength() {
|
|
27
|
+
return this.addressPrefix.length
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get bech32mHrpLength() {
|
|
31
|
+
return this.#config.bech32mHrpLength
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get bootstrap() {
|
|
35
|
+
return this.#bootstrap
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get channel() {
|
|
39
|
+
return this.#channel
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get dhtBootstrap() {
|
|
43
|
+
if (this.#isOverriden('dhtBootstrap')) return this.#options.dhtBootstrap
|
|
44
|
+
return this.#config.dhtBootstrap
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get disableRateLimit() {
|
|
48
|
+
if (this.#isOverriden('disableRateLimit')) return !!this.#options.disableRateLimit
|
|
49
|
+
return !!this.#config.disableRateLimit
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get enableErrorApplyLogs() {
|
|
53
|
+
if (this.#isOverriden('enableErrorApplyLogs')) return !!this.#options.enableErrorApplyLogs
|
|
54
|
+
return !!this.#config.enableErrorApplyLogs
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get enableInteractiveMode() {
|
|
58
|
+
if (this.#isOverriden('enableInteractiveMode')) return this.#options.enableInteractiveMode !== false
|
|
59
|
+
return !!this.#config.enableInteractiveMode
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get enableRoleRequester() {
|
|
63
|
+
if (this.#isOverriden('enableRoleRequester')) return !!this.#options.enableRoleRequester
|
|
64
|
+
return !!this.#config.enableRoleRequester
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get enableValidatorObserver() {
|
|
68
|
+
if (this.#isOverriden('enableValidatorObserver')) return !!this.#options.enableValidatorObserver
|
|
69
|
+
return !!this.#config.enableValidatorObserver
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get enableTxApplyLogs() {
|
|
73
|
+
if (this.#isOverriden('enableTxApplyLogs')) return !!this.#options.enableTxApplyLogs
|
|
74
|
+
return !!this.#config.enableTxApplyLogs
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get enableWallet() {
|
|
78
|
+
if (this.#isOverriden('enableWallet')) return this.#options.enableWallet !== false
|
|
79
|
+
return !!this.#config.enableWallet
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get isAdminMode() {
|
|
83
|
+
return this.#options.storeName === 'admin'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get keyPairPath() {
|
|
87
|
+
return `${this.storesFullPath}/db/keypair.json`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get maxRetries() {
|
|
91
|
+
if (this.#isOverriden('maxRetries')) return this.#options.maxRetries
|
|
92
|
+
return this.#config.maxRetries
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get maxValidators() {
|
|
96
|
+
if (this.#isOverriden('maxValidators')) return this.#options.maxValidators
|
|
97
|
+
return this.#config.maxValidators
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get networkId() {
|
|
101
|
+
return this.#config.networkId
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get storesDirectory() {
|
|
105
|
+
if (this.#isOverriden('storesDirectory')) return this.#options.storesDirectory
|
|
106
|
+
return this.#config.storesDirectory
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get storesFullPath() {
|
|
110
|
+
return `${this.storesDirectory}${this.#options.storeName}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get messageThreshold() {
|
|
114
|
+
return this.#config.messageThreshold
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get messageValidatorRetryDelay() {
|
|
118
|
+
return this.#config.messageValidatorRetryDelay
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get messageValidatorResponseTimeout() {
|
|
122
|
+
return this.#config.messageValidatorResponseTimeout
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Most of these properties are boolean
|
|
126
|
+
#isOverriden(prop) {
|
|
127
|
+
return this.#options.hasOwnProperty(prop)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#validate(options, config) {
|
|
131
|
+
if (!options.channel && !config.channel) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"MainSettlementBus: Channel is required. Application cannot start without channel."
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { TRAC_NETWORK_MSB_MAINNET_PREFIX } from 'trac-wallet/constants.js';
|
|
2
|
+
import { Config } from './config.js';
|
|
3
|
+
|
|
4
|
+
export const ENV = {
|
|
5
|
+
MAINNET: 'mainnet',
|
|
6
|
+
DEVELOPMENT: 'development',
|
|
7
|
+
TESTNET1: 'testnet1'
|
|
8
|
+
}
|
|
9
|
+
// TODO: CREATE TEST ENV CONFIG SIMILAR TO MAINNET AND USE IT IN TESTS.
|
|
10
|
+
// TODO: CREATE TESTNET1 ENV CONFIG and update npm scripts to run node witn mainnet or testnet1.
|
|
11
|
+
|
|
12
|
+
const configData = {
|
|
13
|
+
[ENV.MAINNET]: {
|
|
14
|
+
addressLength: 63,
|
|
15
|
+
addressPrefix: TRAC_NETWORK_MSB_MAINNET_PREFIX,
|
|
16
|
+
addressPrefixLength: TRAC_NETWORK_MSB_MAINNET_PREFIX.length,
|
|
17
|
+
bech32mHrpLength: TRAC_NETWORK_MSB_MAINNET_PREFIX.length + 1, // len(addressPrefix + separator)
|
|
18
|
+
bootstrap: 'acbc3a4344d3a804101d40e53db1dda82b767646425af73599d4cd6577d69685',
|
|
19
|
+
channel: '0000trac0network0msb0mainnet0000',
|
|
20
|
+
dhtBootstrap: ['116.202.214.149:10001', '157.180.12.214:10001', 'node1.hyperdht.org:49737', 'node2.hyperdht.org:49737', 'node3.hyperdht.org:49737'],
|
|
21
|
+
disableRateLimit: false,
|
|
22
|
+
enableErrorApplyLogs: false,
|
|
23
|
+
enableInteractiveMode: true,
|
|
24
|
+
enableRoleRequester: false,
|
|
25
|
+
enableTxApplyLogs: false,
|
|
26
|
+
enableValidatorObserver: true,
|
|
27
|
+
enableWallet: true,
|
|
28
|
+
maxValidators: 50,
|
|
29
|
+
maxRetries: 3,
|
|
30
|
+
messageThreshold: 3,
|
|
31
|
+
messageValidatorRetryDelay: 1000, //How long to wait before retrying (ms) MESSAGE_VALIDATOR_RETRY_DELAY_MS
|
|
32
|
+
messageValidatorResponseTimeout: 3 * 3 * 1000, //Overall timeout for sending a message (ms). This is 3 * maxRetries * messageValidatorRetryDelay;
|
|
33
|
+
networkId: 918,
|
|
34
|
+
storesDirectory: 'stores/',
|
|
35
|
+
},
|
|
36
|
+
[ENV.DEVELOPMENT]: {
|
|
37
|
+
addressLength: 63,
|
|
38
|
+
addressPrefix: TRAC_NETWORK_MSB_MAINNET_PREFIX,
|
|
39
|
+
addressPrefixLength: TRAC_NETWORK_MSB_MAINNET_PREFIX.length,
|
|
40
|
+
bech32mHrpLength: TRAC_NETWORK_MSB_MAINNET_PREFIX.length + 1, // len(addressPrefix + separator)
|
|
41
|
+
bootstrap: 'e90cca53847a12a82f3bf0f67401e45e2ccc1698ee163e61414c2894eb3c6b12',
|
|
42
|
+
channel: '12312313123123',
|
|
43
|
+
dhtBootstrap: ['116.202.214.149:10001', '157.180.12.214:10001', 'node1.hyperdht.org:49737', 'node2.hyperdht.org:49737', 'node3.hyperdht.org:49737'],
|
|
44
|
+
disableRateLimit: false,
|
|
45
|
+
enableErrorApplyLogs: true,
|
|
46
|
+
enableInteractiveMode: true,
|
|
47
|
+
enableRoleRequester: false,
|
|
48
|
+
enableTxApplyLogs: true,
|
|
49
|
+
enableValidatorObserver: true,
|
|
50
|
+
enableWallet: true,
|
|
51
|
+
maxValidators: 6,
|
|
52
|
+
maxRetries: 0,
|
|
53
|
+
messageThreshold: 1000,
|
|
54
|
+
messageValidatorRetryDelay: 1000, //How long to wait before retrying (ms) MESSAGE_VALIDATOR_RETRY_DELAY_MS
|
|
55
|
+
messageValidatorResponseTimeout: 3 * 3 * 1000, //Overall timeout for sending a message (ms). This is 3 * maxRetries * messageValidatorRetryDelay;
|
|
56
|
+
networkId: 918,
|
|
57
|
+
storesDirectory : 'stores/',
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const createConfig = (environment, options) => {
|
|
62
|
+
return new Config(options, configData[environment])
|
|
63
|
+
}
|