diff --git a/msb.mjs b/msb.mjs index 4ae898ff..8b767628 100644 --- a/msb.mjs +++ b/msb.mjs @@ -15,8 +15,8 @@ const rpc = { } const options = args.includes('--rpc') ? rpc : { storeName } - -const msb = new MainSettlementBus(createConfig(ENV.MAINNET, options)); +const config = createConfig(ENV.MAINNET, options) +const msb = new MainSettlementBus(config); msb.ready().then(async () => { if (runRpc) { @@ -25,7 +25,7 @@ msb.ready().then(async () => { const port = (portIndex !== -1 && args[portIndex + 1]) ? parseInt(args[portIndex + 1], 10) : 5000; const hostIndex = args.indexOf('--host'); const host = (hostIndex !== -1 && args[hostIndex + 1]) ? args[hostIndex + 1] : 'localhost'; - startRpcServer(msb, host, port); + startRpcServer(msb, config , host, port); } else { console.log('RPC server will not be started.'); msb.interactiveMode(); diff --git a/proto/network.proto b/proto/network.proto new file mode 100644 index 00000000..1aa13289 --- /dev/null +++ b/proto/network.proto @@ -0,0 +1,74 @@ +syntax = "proto3"; + +package network.v1; + +enum MessageType { + MESSAGE_TYPE_UNSPECIFIED = 0; + MESSAGE_TYPE_VALIDATOR_CONNECTION_REQUEST = 1; + MESSAGE_TYPE_VALIDATOR_CONNECTION_RESPONSE = 2; + MESSAGE_TYPE_LIVENESS_REQUEST = 3; + MESSAGE_TYPE_LIVENESS_RESPONSE = 4; + MESSAGE_TYPE_BROADCAST_TRANSACTION_REQUEST = 5; + MESSAGE_TYPE_BROADCAST_TRANSACTION_RESPONSE = 6; +} + +enum ResultCode { + RESULT_CODE_UNSPECIFIED = 0; + RESULT_CODE_OK = 1; + RESULT_CODE_INVALID_PAYLOAD = 2; + RESULT_CODE_UNSUPPORTED_VERSION = 3; + RESULT_CODE_RATE_LIMITED = 4; + RESULT_CODE_TIMEOUT = 5; + RESULT_CODE_SIGNATURE_INVALID = 6; +} + +message ValidatorConnectionRequest { + string issuer_address = 1; + bytes nonce = 2; + bytes signature = 3; +} + +message ValidatorConnectionResponse { + string issuer_address = 1; + bytes nonce = 2; + bytes signature = 3; + ResultCode result = 4; +} + +message LivenessRequest { + bytes nonce = 1; + bytes signature = 2; +} + +message LivenessResponse { + bytes nonce = 1; + bytes signature = 2; + ResultCode result = 3; +} + +message BroadcastTransactionRequest { + bytes data = 1; // binary encoded payload + bytes nonce = 2; + bytes signature = 3; +} + +message BroadcastTransactionResponse { + bytes nonce = 1; + bytes signature = 2; + ResultCode result = 3; +} + +message MessageHeader { + MessageType type = 1; + uint64 session_id = 2; + uint64 timestamp = 3; + oneof field { + ValidatorConnectionRequest validator_connection_request = 4; + ValidatorConnectionResponse validator_connection_response = 5; + LivenessRequest liveness_request = 6; + LivenessResponse liveness_response = 7; + BroadcastTransactionRequest broadcast_transaction_request = 8; + BroadcastTransactionResponse broadcast_transaction_response = 9; + } + repeated string capabilities = 10; +} diff --git a/rpc/create_server.js b/rpc/create_server.js index a173c25a..dbee578a 100644 --- a/rpc/create_server.js +++ b/rpc/create_server.js @@ -3,7 +3,7 @@ import http from 'http' import { applyCors } from './cors.js'; import { routes } from './routes/index.js'; -export const createServer = (msbInstance) => { +export const createServer = (msbInstance, config) => { const server = http.createServer({}, async (req, res) => { // --- 1. Define safe 'respond' utility (Payload MUST be an object) --- @@ -53,7 +53,7 @@ export const createServer = (msbInstance) => { try { // This try/catch covers synchronous errors and errors from awaited promises // within the route.handler function. - await route.handler({ req, res, respond, msbInstance }); + await route.handler({ req, res, respond, msbInstance, config}); } catch (error) { // Catch errors thrown directly from the handler (or its awaited parts) console.error(`Error on ${route.path}:`, error); diff --git a/rpc/handlers.js b/rpc/handlers.js index 77b7672f..57b1043f 100644 --- a/rpc/handlers.js +++ b/rpc/handlers.js @@ -52,7 +52,7 @@ export async function handleConfirmedLength({ msbInstance, respond }) { respond(200, { confirmed_length }); } -export async function handleBroadcastTransaction({ msbInstance, respond, req }) { +export async function handleBroadcastTransaction({ msbInstance, config, respond, req }) { let body = ''; req.on('data', chunk => { body += chunk.toString(); @@ -72,7 +72,7 @@ export async function handleBroadcastTransaction({ msbInstance, respond, req }) const decodedPayload = decodeBase64Payload(payload); validatePayloadStructure(decodedPayload); const sanitizedPayload = sanitizeTransferPayload(decodedPayload); - const result = await broadcastTransaction(msbInstance, sanitizedPayload); + const result = await broadcastTransaction(msbInstance, config, sanitizedPayload); respond(200, { result }); } catch (error) { let code = error instanceof SyntaxError ? 400 : 500; diff --git a/rpc/rpc_server.js b/rpc/rpc_server.js index b6e52597..c367ac31 100644 --- a/rpc/rpc_server.js +++ b/rpc/rpc_server.js @@ -1,8 +1,8 @@ import { createServer } from "./create_server.js"; // Called by msb.mjs file -export function startRpcServer(msbInstance, host, port) { - const server = createServer(msbInstance) +export function startRpcServer(msbInstance, config ,host, port) { + const server = createServer(msbInstance, config) return server.listen(port, host, () => { console.log(`Running RPC with http at http://${host}:${port}`); diff --git a/rpc/rpc_services.js b/rpc/rpc_services.js index 395dded8..245f9bf7 100644 --- a/rpc/rpc_services.js +++ b/rpc/rpc_services.js @@ -1,6 +1,14 @@ import { bufferToBigInt } from "../src/utils/amountSerialization.js"; -import { normalizeDecodedPayloadForJson } from "../src/utils/normalizers.js"; +import { + normalizeDecodedPayloadForJson, + normalizeTransactionOperation, + normalizeTransferOperation +} from "../src/utils/normalizers.js"; import { get_confirmed_tx_info, get_unconfirmed_tx_info } from "../src/utils/cli.js"; +import {OperationType} from "../src/utils/constants.js"; +import b4a from "b4a"; +import PartialTransaction from "../src/core/network/messaging/validators/PartialTransaction.js"; +import PartialTransfer from "../src/core/network/messaging/validators/PartialTransfer.js"; export async function getBalance(msbInstance, address, confirmed) { const state = msbInstance.state; @@ -36,8 +44,41 @@ export async function getUnconfirmedLength(msbInstance) { return msbInstance.state.getUnsignedLength(); } -export async function broadcastTransaction(msbInstance, payload) { - return msbInstance.broadcastTransactionCommand(payload); +export async function broadcastTransaction(msbInstance, config, payload) { + if (!payload) { + throw new Error("Transaction payload is required for broadcasting."); + } + let normalizedPayload; + let isValid = false; + let hash; + + const partialTransferValidator = new PartialTransfer(msbInstance.state, null , config); + const partialTransactionValidator = new PartialTransaction(msbInstance.state, null , config); + + if (payload.type === OperationType.TRANSFER) { + normalizedPayload = normalizeTransferOperation(payload, config); + isValid = await partialTransferValidator.validate(normalizedPayload); + hash = b4a.toString(normalizedPayload.tro.tx, "hex"); + } else if (payload.type === OperationType.TX) { + normalizedPayload = normalizeTransactionOperation(payload, config); + isValid = await partialTransactionValidator.validate(normalizedPayload); + hash = b4a.toString(normalizedPayload.txo.tx, "hex"); + } + + if (!isValid) { + throw new Error("Invalid transaction payload."); + } + + const success = await msbInstance.broadcastPartialTransaction(payload); + + if (!success) { + throw new Error("Failed to broadcast transaction after multiple attempts."); + } + + const signedLength = msbInstance.state.getSignedLength(); + const unsignedLength = msbInstance.state.getUnsignedLength(); + + return { message: "Transaction broadcasted successfully.", signedLength, unsignedLength, tx: hash }; } export async function getTxHashes(msbInstance, start, end) { diff --git a/rpc/utils/helpers.js b/rpc/utils/helpers.js index 73945af4..f13f0d31 100644 --- a/rpc/utils/helpers.js +++ b/rpc/utils/helpers.js @@ -1,5 +1,5 @@ import b4a from "b4a" -import { operationToPayload } from "../../src/utils/operations.js" +import { operationToPayload } from "../../src/utils/applyOperations.js" export function decodeBase64Payload(base64) { let decodedPayloadString try { diff --git a/src/core/network/identity/NetworkWalletFactory.js b/src/core/network/identity/NetworkWalletFactory.js index de3ac35b..17f285aa 100644 --- a/src/core/network/identity/NetworkWalletFactory.js +++ b/src/core/network/identity/NetworkWalletFactory.js @@ -1,7 +1,7 @@ import PeerWallet from 'trac-wallet'; import b4a from 'b4a'; -export class NetworkWalletFactory { +class NetworkWalletFactory { static provide(options = {}) { const { enableWallet, @@ -28,7 +28,7 @@ export class NetworkWalletFactory { // TODO: Once Wallet class in trac-wallet exposes a constructor/factory that accepts an existing keyPair // (e.g. Wallet.fromKeyPair({ publicKey, secretKey }, networkPrefix)), replace EphemeralWallet // with a thin wrapper around that functionality instead of duplicating signing/verification logic. -class EphemeralWallet { +export class EphemeralWallet { #publicKey; #secretKey; #address; diff --git a/src/core/network/messaging/handlers/RoleOperationHandler.js b/src/core/network/messaging/handlers/RoleOperationHandler.js index c5fed778..fe879dcc 100644 --- a/src/core/network/messaging/handlers/RoleOperationHandler.js +++ b/src/core/network/messaging/handlers/RoleOperationHandler.js @@ -1,10 +1,9 @@ import {OperationType} from '../../../../utils/constants.js'; import PartialRoleAccess from "../validators/PartialRoleAccess.js"; -import {addressToBuffer} from "../../../state/utils/address.js"; -import CompleteStateMessageOperations - from "../../../../messages/completeStateMessages/CompleteStateMessageOperations.js"; -import {normalizeHex} from "../../../../utils/helpers.js"; import BaseOperationHandler from './base/BaseOperationHandler.js'; +import {applyStateMessageFactory} from "../../../../messages/state/applyStateMessageFactory.js"; +import {safeEncodeApplyOperation} from "../../../../utils/protobuf/operationHelpers.js"; +import {normalizeRoleAccessOperation} from "../../../../utils/normalizers.js"; class RoleOperationHandler extends BaseOperationHandler { #partialRoleAccessValidator; @@ -24,7 +23,7 @@ class RoleOperationHandler extends BaseOperationHandler { this.#wallet = wallet; this.#config = config; this.#network = network; - this.#partialRoleAccessValidator = new PartialRoleAccess(state, wallet ,this.#config) + this.#partialRoleAccessValidator = new PartialRoleAccess(state, this.#wallet.address ,this.#config) } get partialRoleAccessValidator() { @@ -32,7 +31,7 @@ class RoleOperationHandler extends BaseOperationHandler { } async handleOperation(message, connection) { - const normalizedPartialRoleAccessPayload = this.#normalizePartialRoleAccess(message) + const normalizedPartialRoleAccessPayload = normalizeRoleAccessOperation(message, this.#config) const isValid = await this.partialRoleAccessValidator.validate(normalizedPartialRoleAccessPayload) let completePayload = null if (!isValid) { @@ -41,35 +40,38 @@ class RoleOperationHandler extends BaseOperationHandler { switch (normalizedPartialRoleAccessPayload.type) { case OperationType.ADD_WRITER: - completePayload = await new CompleteStateMessageOperations(this.#wallet, this.#config).assembleAddWriterMessage( - normalizedPartialRoleAccessPayload.address, - normalizedPartialRoleAccessPayload.rao.tx, - normalizedPartialRoleAccessPayload.rao.txv, - normalizedPartialRoleAccessPayload.rao.iw, - normalizedPartialRoleAccessPayload.rao.in, - normalizedPartialRoleAccessPayload.rao.is, - ); + completePayload = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteAddWriterMessage( + normalizedPartialRoleAccessPayload.address, + normalizedPartialRoleAccessPayload.rao.tx, + normalizedPartialRoleAccessPayload.rao.txv, + normalizedPartialRoleAccessPayload.rao.iw, + normalizedPartialRoleAccessPayload.rao.in, + normalizedPartialRoleAccessPayload.rao.is, + ) break; case OperationType.REMOVE_WRITER: - completePayload = await new CompleteStateMessageOperations(this.#wallet, this.#config).assembleRemoveWriterMessage( - normalizedPartialRoleAccessPayload.address, - normalizedPartialRoleAccessPayload.rao.tx, - normalizedPartialRoleAccessPayload.rao.txv, - normalizedPartialRoleAccessPayload.rao.iw, - normalizedPartialRoleAccessPayload.rao.in, - normalizedPartialRoleAccessPayload.rao.is, - ); + completePayload = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteRemoveWriterMessage( + normalizedPartialRoleAccessPayload.address, + normalizedPartialRoleAccessPayload.rao.tx, + normalizedPartialRoleAccessPayload.rao.txv, + normalizedPartialRoleAccessPayload.rao.iw, + normalizedPartialRoleAccessPayload.rao.in, + normalizedPartialRoleAccessPayload.rao.is, + ) break; case OperationType.ADMIN_RECOVERY: - completePayload = await new CompleteStateMessageOperations(this.#wallet, this.#config).assembleAdminRecoveryMessage( - normalizedPartialRoleAccessPayload.address, - normalizedPartialRoleAccessPayload.rao.tx, - normalizedPartialRoleAccessPayload.rao.txv, - normalizedPartialRoleAccessPayload.rao.iw, - normalizedPartialRoleAccessPayload.rao.in, - normalizedPartialRoleAccessPayload.rao.is, - ); - console.log("Assembled complete role access operation:", completePayload); + + completePayload = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteAdminRecoveryMessage( + normalizedPartialRoleAccessPayload.address, + normalizedPartialRoleAccessPayload.rao.tx, + normalizedPartialRoleAccessPayload.rao.txv, + normalizedPartialRoleAccessPayload.rao.iw, + normalizedPartialRoleAccessPayload.rao.in, + normalizedPartialRoleAccessPayload.rao.is, + ) break; default: throw new Error("OperationHandler: Assembling complete role access operation failed due to unsupported operation type."); @@ -79,35 +81,7 @@ class RoleOperationHandler extends BaseOperationHandler { throw new Error("OperationHandler: Assembling complete role access operation failed."); } - this.#network.transactionPoolService.addTransaction(completePayload) - } - - #normalizePartialRoleAccess(payload) { - if (!payload || typeof payload !== 'object' || !payload.rao) { - throw new Error('Invalid payload for bootstrap deployment normalization.'); - } - const {type, address, rao} = payload; - if ( - !type || - !address || - !rao.tx || !rao.txv || !rao.iw || !rao.in || !rao.is - ) { - throw new Error('Missing required fields in bootstrap deployment payload.'); - } - - const normalizedRao = { - tx: normalizeHex(rao.tx), - txv: normalizeHex(rao.txv), - iw: normalizeHex(rao.iw), - in: normalizeHex(rao.in), - is: normalizeHex(rao.is) - }; - - return { - type, - address: addressToBuffer(address, this.#config.addressPrefix), - rao: normalizedRao - }; + this.#network.transactionPoolService.addTransaction(safeEncodeApplyOperation(completePayload)) } } diff --git a/src/core/network/messaging/handlers/SubnetworkOperationHandler.js b/src/core/network/messaging/handlers/SubnetworkOperationHandler.js index 55b624e2..413ed895 100644 --- a/src/core/network/messaging/handlers/SubnetworkOperationHandler.js +++ b/src/core/network/messaging/handlers/SubnetworkOperationHandler.js @@ -1,13 +1,15 @@ import BaseOperationHandler from './base/BaseOperationHandler.js'; -import CompleteStateMessageOperations - from "../../../../messages/completeStateMessages/CompleteStateMessageOperations.js"; import { OperationType } from '../../../../utils/constants.js'; import PartialBootstrapDeployment from "../validators/PartialBootstrapDeployment.js"; -import {addressToBuffer} from "../../../state/utils/address.js"; import PartialTransaction from "../validators/PartialTransaction.js"; -import {normalizeHex} from "../../../../utils/helpers.js"; +import {applyStateMessageFactory} from "../../../../messages/state/applyStateMessageFactory.js"; +import {safeEncodeApplyOperation} from "../../../../utils/protobuf/operationHelpers.js"; +import { + normalizeBootstrapDeploymentOperation, + normalizeTransactionOperation +} from "../../../../utils/normalizers.js"; class SubnetworkOperationHandler extends BaseOperationHandler { @@ -27,8 +29,8 @@ class SubnetworkOperationHandler extends BaseOperationHandler { super(network, state, wallet, rateLimiter, config); this.#config = config this.#wallet = wallet - this.#partialBootstrapDeploymentValidator = new PartialBootstrapDeployment(state, wallet, config); - this.#partialTransactionValidator = new PartialTransaction(state, wallet, config); + this.#partialBootstrapDeploymentValidator = new PartialBootstrapDeployment(state, this.#wallet.address, config); + this.#partialTransactionValidator = new PartialTransaction(state, this.#wallet.address, config); } async handleOperation(payload) { @@ -42,14 +44,14 @@ class SubnetworkOperationHandler extends BaseOperationHandler { } async #partialTransactionSubHandler(payload) { - const normalizedPayload = this.#normalizeTransactionOperation(payload, this.#config); + const normalizedPayload = normalizeTransactionOperation(payload, this.#config); const isValid = await this.#partialTransactionValidator.validate(normalizedPayload); if (!isValid) { throw new Error("SubnetworkHandler: Transaction validation failed."); } - const completeTransactionOperation = await new CompleteStateMessageOperations(this.#wallet, this.#config) - .assembleCompleteTransactionOperationMessage( + const completeTransactionOperation = await applyStateMessageFactory(this.#wallet,this.#config) + .buildCompleteTransactionOperationMessage( normalizedPayload.address, normalizedPayload.txo.tx, normalizedPayload.txo.txv, @@ -59,90 +61,30 @@ class SubnetworkOperationHandler extends BaseOperationHandler { normalizedPayload.txo.is, normalizedPayload.txo.bs, normalizedPayload.txo.mbs - ); - this.network.transactionPoolService.addTransaction(completeTransactionOperation); + ) + this.network.transactionPoolService.addTransaction(safeEncodeApplyOperation(completeTransactionOperation)); } async #partialBootstrapDeploymentSubHandler(payload) { - const normalizedPayload = this.#normalizeBootstrapDeployment(payload); + const normalizedPayload = normalizeBootstrapDeploymentOperation(payload, this.#config); const isValid = await this.#partialBootstrapDeploymentValidator.validate(normalizedPayload); if (!isValid) { throw new Error("SubnetworkHandler: Bootstrap deployment validation failed."); } - const completeBootstrapDeploymentOperation = await new CompleteStateMessageOperations(this.#wallet, this.#config) - .assembleCompleteBootstrapDeployment( + + const completeBootstrapDeploymentOperation = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteBootstrapDeploymentMessage( normalizedPayload.address, normalizedPayload.bdo.tx, normalizedPayload.bdo.txv, normalizedPayload.bdo.bs, normalizedPayload.bdo.ic, normalizedPayload.bdo.in, - normalizedPayload.bdo.is, + normalizedPayload.bdo.is ) - this.network.transactionPoolService.addTransaction(completeBootstrapDeploymentOperation); - - } - - #normalizeBootstrapDeployment(payload) { - if (!payload || typeof payload !== 'object' || !payload.bdo) { - throw new Error('Invalid payload for bootstrap deployment normalization.'); - } - const {type, address, bdo} = payload; - if ( - type !== OperationType.BOOTSTRAP_DEPLOYMENT || - !address || - !bdo.tx || !bdo.bs || !bdo.in || !bdo.is || !bdo.txv - ) { - throw new Error('Missing required fields in bootstrap deployment payload.'); - } - - const normalizedBdo = { - tx: normalizeHex(bdo.tx), // Transaction hash - txv: normalizeHex(bdo.txv), // Transaction validity - bs: normalizeHex(bdo.bs), // External bootstrap - ic: normalizeHex(bdo.ic), // Channel - in: normalizeHex(bdo.in), // Nonce - is: normalizeHex(bdo.is) // Signature - }; - - return { - type, - address: addressToBuffer(address, this.#config.addressPrefix), - bdo: normalizedBdo - }; - } - - #normalizeTransactionOperation(payload) { - if (!payload || typeof payload !== 'object' || !payload.txo) { - throw new Error('Invalid payload for transaction operation normalization.'); - } - const {type, address, txo} = payload; - if ( - type !== OperationType.TX || - !address || - !txo.tx || !txo.txv || !txo.iw || !txo.in || - !txo.ch || !txo.is || !txo.bs || !txo.mbs - ) { - throw new Error('Missing required fields in transaction operation payload.'); - } - - const normalizedTxo = { - tx: normalizeHex(txo.tx), // Transaction hash - txv: normalizeHex(txo.txv), // Transaction validity - iw: normalizeHex(txo.iw), // Writing key - in: normalizeHex(txo.in), // Nonce - ch: normalizeHex(txo.ch), // Content hash - is: normalizeHex(txo.is), // Signature - bs: normalizeHex(txo.bs), // External bootstrap - mbs: normalizeHex(txo.mbs) // MSB bootstrap key - }; + this.network.transactionPoolService.addTransaction(safeEncodeApplyOperation(completeBootstrapDeploymentOperation)); - return { - type, - address: addressToBuffer(address, this.#config.addressPrefix), - txo: normalizedTxo - }; } } diff --git a/src/core/network/messaging/handlers/TransferOperationHandler.js b/src/core/network/messaging/handlers/TransferOperationHandler.js index 33f13da9..1a211279 100644 --- a/src/core/network/messaging/handlers/TransferOperationHandler.js +++ b/src/core/network/messaging/handlers/TransferOperationHandler.js @@ -1,9 +1,9 @@ import BaseOperationHandler from './base/BaseOperationHandler.js'; -import CompleteStateMessageOperations - from "../../../../messages/completeStateMessages/CompleteStateMessageOperations.js"; import {OperationType} from '../../../../utils/constants.js'; import PartialTransfer from "../validators/PartialTransfer.js"; import {normalizeTransferOperation} from "../../../../utils/normalizers.js" +import {applyStateMessageFactory} from "../../../../messages/state/applyStateMessageFactory.js"; +import {safeEncodeApplyOperation} from "../../../../utils/protobuf/operationHelpers.js"; class TransferOperationHandler extends BaseOperationHandler { #partialTransferValidator; @@ -21,7 +21,7 @@ class TransferOperationHandler extends BaseOperationHandler { super(network, state, wallet, rateLimiter, config); this.#config = config; this.#wallet = wallet; - this.#partialTransferValidator = new PartialTransfer(state, wallet, this.#config); + this.#partialTransferValidator = new PartialTransfer(state, this.#wallet.address, this.#config); } async handleOperation(payload) { @@ -38,8 +38,8 @@ class TransferOperationHandler extends BaseOperationHandler { throw new Error("TransferHandler: Transfer validation failed."); } - const completeTransferOperation = await new CompleteStateMessageOperations(this.#wallet, this.#config) - .assembleCompleteTransferOperationMessage( + const completeTransferOperation = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteTransferOperationMessage( normalizedPayload.address, normalizedPayload.tro.tx, normalizedPayload.tro.txv, @@ -47,9 +47,9 @@ class TransferOperationHandler extends BaseOperationHandler { normalizedPayload.tro.to, normalizedPayload.tro.am, normalizedPayload.tro.is - ); + ) - this.network.transactionPoolService.addTransaction(completeTransferOperation); + this.network.transactionPoolService.addTransaction(safeEncodeApplyOperation(completeTransferOperation)); } } diff --git a/src/core/network/messaging/routes/NetworkMessageRouter.js b/src/core/network/messaging/routes/NetworkMessageRouter.js index 96b117e3..ef1fae72 100644 --- a/src/core/network/messaging/routes/NetworkMessageRouter.js +++ b/src/core/network/messaging/routes/NetworkMessageRouter.js @@ -5,7 +5,7 @@ import RoleOperationHandler from "../handlers/RoleOperationHandler.js"; import SubnetworkOperationHandler from "../handlers/SubnetworkOperationHandler.js"; import TransferOperationHandler from "../handlers/TransferOperationHandler.js"; import {NETWORK_MESSAGE_TYPES} from '../../../../utils/constants.js'; -import * as operation from '../../../../utils/operations.js'; +import * as operation from '../../../../utils/applyOperations.js'; import TransactionRateLimiterService from "../../services/TransactionRateLimiterService.js"; import State from "../../../state/State.js"; import PeerWallet from "trac-wallet"; @@ -37,9 +37,6 @@ class NetworkMessageRouter { async route(incomingMessage, connection, messageProtomux) { try { - // TODO: Add a check here — only a writer should be able to process the handlers isRoleAccessOperation,isSubnetworkOperation - // and admin nodes until the writers' index is less than 25. OperationType.APPEND_WHITELIST can be processed by only READERS - const channelString = b4a.toString(this.#config.channel, 'utf8'); if (this.#isGetRequest(incomingMessage)) { await this.#handlers.get.handle(incomingMessage, messageProtomux, connection, channelString); diff --git a/src/core/network/messaging/validators/PartialBootstrapDeployment.js b/src/core/network/messaging/validators/PartialBootstrapDeployment.js index 46f21c32..168bb77b 100644 --- a/src/core/network/messaging/validators/PartialBootstrapDeployment.js +++ b/src/core/network/messaging/validators/PartialBootstrapDeployment.js @@ -1,8 +1,8 @@ import PartialOperation from './base/PartialOperation.js'; class PartialBootstrapDeployment extends PartialOperation { - constructor(state, wallet , config) { - super(state, wallet , config); + constructor(state, selfAddress , config) { + super(state, selfAddress , config); } async validate(payload) { diff --git a/src/core/network/messaging/validators/PartialRoleAccess.js b/src/core/network/messaging/validators/PartialRoleAccess.js index fcd75517..6336fa8d 100644 --- a/src/core/network/messaging/validators/PartialRoleAccess.js +++ b/src/core/network/messaging/validators/PartialRoleAccess.js @@ -7,8 +7,8 @@ import {bufferToBigInt} from "../../../../utils/amountSerialization.js"; class PartialRoleAccess extends PartialOperation { #config; - constructor(state, wallet, config) { - super(state, wallet, config); + constructor(state, selfAddress, config) { + super(state, selfAddress, config); this.#config = config } diff --git a/src/core/network/messaging/validators/PartialTransaction.js b/src/core/network/messaging/validators/PartialTransaction.js index 14212ed1..dd126ed2 100644 --- a/src/core/network/messaging/validators/PartialTransaction.js +++ b/src/core/network/messaging/validators/PartialTransaction.js @@ -6,8 +6,8 @@ import PartialOperation from './base/PartialOperation.js'; class PartialTransaction extends PartialOperation { #config - constructor(state, wallet, config) { - super(state, wallet, config); + constructor(state, selfAddress, config) { + super(state, selfAddress, config); this.#config = config } diff --git a/src/core/network/messaging/validators/PartialTransfer.js b/src/core/network/messaging/validators/PartialTransfer.js index b9406036..6ccef07a 100644 --- a/src/core/network/messaging/validators/PartialTransfer.js +++ b/src/core/network/messaging/validators/PartialTransfer.js @@ -7,8 +7,8 @@ import PartialOperation from './base/PartialOperation.js'; class PartialTransfer extends PartialOperation { #config - constructor(state, wallet, config) { - super(state, wallet, config); + constructor(state, selfAddress, config) { + super(state, selfAddress, config); this.#config = config } diff --git a/src/core/network/messaging/validators/base/PartialOperation.js b/src/core/network/messaging/validators/base/PartialOperation.js index 2a7232de..2669a5ae 100644 --- a/src/core/network/messaging/validators/base/PartialOperation.js +++ b/src/core/network/messaging/validators/base/PartialOperation.js @@ -6,7 +6,7 @@ import {createMessage} from "../../../../../utils/buffer.js"; import {OperationType} from "../../../../../utils/constants.js"; import {bufferToBigInt} from "../../../../../utils/amountSerialization.js"; import {FEE} from "../../../../state/utils/transaction.js"; -import * as operationsUtils from '../../../../../utils/operations.js'; +import * as operationsUtils from '../../../../../utils/applyOperations.js'; const MAX_AMOUNT = BigInt('0xffffffffffffffffffffffffffffffff'); const FEE_BIGINT = bufferToBigInt(FEE); @@ -16,15 +16,15 @@ class PartialOperation { #state; #check; #config - #wallet + #selfAddress - constructor(state, wallet, config) { + constructor(state, selfAddress, config) { this.#state = state; this.#config = config; this.#check = new Check(this.#config); this.max_amount = MAX_AMOUNT; this.fee = FEE_BIGINT; - this.#wallet = wallet; + this.#selfAddress = selfAddress; } get state() { @@ -176,7 +176,7 @@ class PartialOperation { isOperationNotCompleted(payload) { const operationKey = operationsUtils.operationToPayload(payload.type); const operation = payload[operationKey]; - const {va, vn, vs} = operation; + const { va, vn, vs } = operation; const condition = va === undefined && vn === undefined && vs === undefined if (!condition) { @@ -219,8 +219,10 @@ class PartialOperation { * Flow: Validator -> submits tx with tap-wallet -> RPC-> Validator -validates tx-> REJECT (self-validation) */ validateNoSelfValidation(payload) { + if (!this.#selfAddress) return; + const requesterAddress = bufferToAddress(payload.address, this.#config.addressPrefix); - if (this.#wallet.address === requesterAddress) { + if (this.#selfAddress === requesterAddress) { throw new Error('Requester address cannot be the same as the validator wallet address.'); } } diff --git a/src/core/network/services/MessageOrchestrator.js b/src/core/network/services/MessageOrchestrator.js index 0697acca..e3a19b8a 100644 --- a/src/core/network/services/MessageOrchestrator.js +++ b/src/core/network/services/MessageOrchestrator.js @@ -1,5 +1,5 @@ import { sleep } from '../../../utils/helpers.js'; -import { operationToPayload } from '../../../utils/operations.js'; +import { operationToPayload } from '../../../utils/applyOperations.js'; /** * MessageOrchestrator coordinates message submission, retry, and validator management. * It works with ConnectionManager and ledger state to ensure reliable message delivery. diff --git a/src/core/state/State.js b/src/core/state/State.js index dca71b76..e8e7a640 100644 --- a/src/core/state/State.js +++ b/src/core/state/State.js @@ -201,8 +201,7 @@ class State extends ReadyResource { } async getIndexersEntry() { - const indexersEntry = Object.values(this.#base.system.indexers); - return indexersEntry + return Object.values(this.#base.system.indexers); } async isWkInIndexersEntry(wk) { diff --git a/src/index.js b/src/index.js index ba4d3bb5..e9252b56 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,3 @@ - /** @typedef {import('pear-interface')} */ /* global Pear */ import ReadyResource from "ready-resource"; import Corestore from "corestore"; @@ -8,26 +7,21 @@ import readline from "readline"; import tty from "tty"; import { sleep, isHexString } from "./utils/helpers.js"; import { verifyDag, printHelp, printWalletInfo, printBalance } from "./utils/cli.js"; -import CompleteStateMessageOperations from "./messages/completeStateMessages/CompleteStateMessageOperations.js"; +import { applyStateMessageFactory } from "./messages/state/applyStateMessageFactory.js"; import { isAddressValid } from "./core/state/utils/address.js"; import Network from "./core/network/Network.js"; import Check from "./utils/check.js"; import State from "./core/state/State.js"; -import PartialStateMessageOperations from "./messages/partialStateMessages/PartialStateMessageOperations.js"; import { EventType, WHITELIST_SLEEP_INTERVAL, BOOTSTRAP_HEXSTRING_LENGTH, - OperationType, CustomEventType, BALANCE_MIGRATION_SLEEP_INTERVAL, WHITELIST_MIGRATION_DIR } from "./utils/constants.js"; import { randomBytes } from "hypercore-crypto"; import { decimalStringToBigInt, bigIntTo16ByteBuffer, bufferToBigInt, bigIntToDecimalString } from "./utils/amountSerialization.js" -import { normalizeTransferOperation, normalizeTransactionOperation } from "./utils/normalizers.js" -import PartialTransfer from "./core/network/messaging/validators/PartialTransfer.js"; -import PartialTransaction from "./core/network/messaging/validators/PartialTransaction.js"; import fileUtils from './utils/fileUtils.js'; import migrationUtils from './utils/migrationUtils.js'; import { @@ -49,16 +43,15 @@ import { getLicenseAddressCommand, getLicenseCountCommand } from "./utils/cliCommands.js"; +import {safeEncodeApplyOperation} from "./utils/protobuf/operationHelpers.js"; + export class MainSettlementBus extends ReadyResource { - // internal attributes #store; #wallet; #network; #readline_instance; #state; #isClosing = false; - #partialTransferValidator; - #partialTransactionValidator; #config /** @@ -67,7 +60,6 @@ export class MainSettlementBus extends ReadyResource { constructor(config) { super(); this.#config = config - this.#store = new Corestore(this.#config.storesFullPath); this.#wallet = new PeerWallet({ networkPrefix: this.#config.addressPrefix }); this.#readline_instance = null; @@ -96,8 +88,10 @@ export class MainSettlementBus extends ReadyResource { return this.#network; } - // This can be null if enable_wallet is false get wallet() { + if (!this.#config.enableWallet) { + return undefined; + } return this.#wallet; } @@ -113,15 +107,12 @@ export class MainSettlementBus extends ReadyResource { await this.#state.ready(); await this.#network.ready(); - this.#stateEventsListener(); + await this.#stateEventsListener(); if (this.#config.enableWallet) { printWalletInfo(this.#wallet.address, this.#state.writingKey, this.#state, this.#config.enableWallet); } - this.#partialTransferValidator = new PartialTransfer(this.state, this.#wallet, this.#config); - this.#partialTransactionValidator = new PartialTransaction(this.state, this.#wallet ,this.#config); - await this.#network.replicate( this.#state, this.#store, @@ -131,6 +122,8 @@ export class MainSettlementBus extends ReadyResource { const adminEntry = await this.#state.getAdminEntry(); await this.#setUpRoleAutomatically(adminEntry); + + console.log(`isIndexer: ${this.#state.isIndexer()}`); console.log(`isWriter: ${this.#state.isWritable()}`); console.log("MSB Unsigned Length:", this.#state.getUnsignedLength()); @@ -182,41 +175,6 @@ export class MainSettlementBus extends ReadyResource { return await this.#network.validatorMessageOrchestrator.send(partialTransactionPayload); } - async broadcastTransactionCommand(payload) { - if (!payload) { - throw new Error("Transaction payload is required for broadcast_transaction command."); - } - - let normalizedPayload; - let isValid = false; - let hash; - - if (payload.type === OperationType.TRANSFER) { - normalizedPayload = normalizeTransferOperation(payload, this.#config); - isValid = await this.#partialTransferValidator.validate(normalizedPayload); - hash = b4a.toString(normalizedPayload.tro.tx, "hex"); - } else if (payload.type === OperationType.TX) { - normalizedPayload = normalizeTransactionOperation(payload, this.#config); - isValid = await this.#partialTransactionValidator.validate(normalizedPayload); - hash = b4a.toString(normalizedPayload.txo.tx, "hex"); - } - - if (!isValid) { - throw new Error("Invalid transaction payload."); - } - - const signedLength = this.#state.getSignedLength(); - const unsignedLength = this.#state.getUnsignedLength(); - - const success = await this.broadcastPartialTransaction(payload); - - if (!success) { - throw new Error("Failed to broadcast transaction after multiple attempts."); - } - - return { message: "Transaction broadcasted successfully.", signedLength, unsignedLength, tx: hash }; - } - async #setUpRoleAutomatically() { if (!this.#state.isWritable() && this.#config.enableRoleRequester) { console.log("Requesting writer role... This may take a moment."); @@ -296,14 +254,17 @@ export class MainSettlementBus extends ReadyResource { } const txValidity = await PeerWallet.blake3(this.#config.bootstrap); - const addAdminMessage = await new CompleteStateMessageOperations(this.#wallet, this.#config) - .assembleAddAdminMessage( + const addAdminMessage = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteAddAdminMessage( + this.#wallet.address, this.#state.writingKey, - txValidity - ); + txValidity, + ) + const encodedPayload = safeEncodeApplyOperation(addAdminMessage); - await this.#state.append(addAdminMessage); + await this.#state.append(encodedPayload); } + async #handleAdminRecovery() { if (!this.#config.enableWallet) { throw new Error("Can not initialize an admin - wallet is not enabled."); @@ -329,10 +290,13 @@ export class MainSettlementBus extends ReadyResource { } const txValidity = await this.#state.getIndexerSequenceState(); - const adminRecoveryMessage = await new PartialStateMessageOperations(this.#wallet, this.#config).assembleAdminRecoveryMessage( - this.#state.writingKey.toString('hex'), - txValidity.toString('hex') - ); + const adminRecoveryMessage = await applyStateMessageFactory(this.#wallet, this.#config) + .buildPartialAdminRecoveryMessage( + this.#wallet.address, + this.#state.writingKey, + txValidity, + "json" + ) const success = await this.broadcastPartialTransaction(adminRecoveryMessage); @@ -366,12 +330,13 @@ export class MainSettlementBus extends ReadyResource { for (const addressToWhitelist of addresses) { const txValidity = await this.#state.getIndexerSequenceState(); - const encodedPayload = await new CompleteStateMessageOperations(this.#wallet, this.#config) - .assembleAppendWhitelistMessages( + const appendWhitelistMessage = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteAppendWhitelistMessage( + this.#wallet.address, + addressToWhitelist, txValidity, - addressToWhitelist - ); - + ) + const encodedPayload = safeEncodeApplyOperation(appendWhitelistMessage) messages.set(addressToWhitelist, encodedPayload); } @@ -445,10 +410,13 @@ export class MainSettlementBus extends ReadyResource { } const txValidity = await this.#state.getIndexerSequenceState(); - const assembledMessage = await new PartialStateMessageOperations(this.#wallet, { networkId: this.#config.networkId, addressPrefix: this.#config.addressPrefix }) - .assembleAddWriterMessage( - this.#state.writingKey.toString('hex'), - txValidity.toString('hex') + + const assembledMessage = await applyStateMessageFactory(this.#wallet, this.#config) + .buildPartialAddWriterMessage( + this.#wallet.address, + this.#state.writingKey, + txValidity, + 'json' ) const success = await this.broadcastPartialTransaction(assembledMessage); @@ -477,10 +445,12 @@ export class MainSettlementBus extends ReadyResource { } const txValidity = await this.#state.getIndexerSequenceState(); - const assembledMessage = await new PartialStateMessageOperations(this.#wallet, { networkId: this.#config.networkId, addressPrefix: this.#config.addressPrefix }) - .assembleRemoveWriterMessage( - nodeEntry.wk.toString('hex'), - txValidity.toString('hex') + const assembledMessage = await applyStateMessageFactory(this.#wallet, this.#config) + .buildPartialRemoveWriterMessage( + this.#wallet.address, + nodeEntry.wk, + txValidity, + "json" ) const success = await this.broadcastPartialTransaction(assembledMessage); @@ -492,10 +462,10 @@ export class MainSettlementBus extends ReadyResource { console.info(`Transaction hash: ${assembledMessage.rao.tx}`); } - async #updateIndexerRole(address, toAdd) { + async #updateWriterToIndexerRole(addressToUpdate, toAdd) { if (!this.#config.enableWallet) { throw new Error( - `Can not request indexer role for: ${address} - wallet is not enabled.` + `Can not request indexer role for: ${addressToUpdate} - wallet is not enabled.` ); } @@ -503,29 +473,29 @@ export class MainSettlementBus extends ReadyResource { if (!adminEntry) { throw new Error( - `Can not request indexer role for: ${address} - admin entry has not been initialized.` + `Can not request indexer role for: ${addressToUpdate} - admin entry has not been initialized.` ); } - if (!isAddressValid(address, this.#config.addressPrefix)) { + if (!isAddressValid(addressToUpdate, this.#config.addressPrefix)) { throw new Error( - `Can not request indexer role for: ${address} - invalid address.` + `Can not request indexer role for: ${addressToUpdate} - invalid address.` ); } if (!this.#isAdmin(adminEntry) && !this.#state.isWritable()) { throw new Error( - `Can not request indexer role for: ${address} - You are not an admin or writer.` + `Can not request indexer role for: ${addressToUpdate} - You are not an admin or writer.` ); } - const nodeEntry = await this.#state.getNodeEntry(address); + const nodeEntry = await this.#state.getNodeEntry(addressToUpdate); if (!nodeEntry) { throw new Error( - `Can not request indexer role for: ${address} - node entry has not been not initialized.` + `Can not request indexer role for: ${addressToUpdate} - node entry has not been not initialized.` ); } - const indexerNodeEntry = await this.#state.getNodeEntry(address); + const indexerNodeEntry = await this.#state.getNodeEntry(addressToUpdate); const indexerListHasAddress = await this.#state.isWkInIndexersEntry( indexerNodeEntry.wk, ); @@ -533,7 +503,7 @@ export class MainSettlementBus extends ReadyResource { if (toAdd) { if (indexerListHasAddress) { throw new Error( - `Cannot update indexer role for: ${address} - address is already in indexers list.` + `Cannot update indexer role for: ${addressToUpdate} - address is already in indexers list.` ); } @@ -545,71 +515,86 @@ export class MainSettlementBus extends ReadyResource { if (!canAddIndexer) { throw new Error( - `Can not request indexer role for: ${address} - node is not whitelisted, not a writer or already an indexer.` + `Can not request indexer role for: ${addressToUpdate} - node is not whitelisted, not a writer or already an indexer.` ); } const txValidity = await this.#state.getIndexerSequenceState(); - const assembledAddIndexerMessage = await new CompleteStateMessageOperations(this.#wallet, this.#config) - .assembleAddIndexerMessage(address, txValidity); - await this.#state.append(assembledAddIndexerMessage); + + const assembledAddIndexerMessage = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteAddIndexerMessage( + this.#wallet.address, + addressToUpdate, + txValidity, + ) + + const encodedPayload = safeEncodeApplyOperation(assembledAddIndexerMessage); + + await this.#state.append(encodedPayload); } else { const canRemoveIndexer = !toAdd && nodeEntry.isIndexer && indexerListHasAddress; if (!canRemoveIndexer) { throw new Error( - `Can not remove indexer role for: ${address} - node is not an indexer or address is not in indexers list.` + `Can not remove indexer role for: ${addressToUpdate} - node is not an indexer or address is not in indexers list.` ); } const txValidity = await this.#state.getIndexerSequenceState(); - const assembledRemoveIndexer = await new CompleteStateMessageOperations(this.#wallet, this.#config) - .assembleRemoveIndexerMessage(address, txValidity); + const assembledRemoveIndexerMessage = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteRemoveIndexerMessage( + this.#wallet.address, + addressToUpdate, + txValidity, + ) + const encodedPayload = safeEncodeApplyOperation(assembledRemoveIndexerMessage); - await this.#state.append(assembledRemoveIndexer); + await this.#state.append(encodedPayload); } } - async #banValidator(address) { + async #banValidator(addresstToBan) { if (!this.#config.enableWallet) { throw new Error( - `Can not ban writer with address: ${address} - wallet is not enabled.` + `Can not ban writer with address: ${addresstToBan} - wallet is not enabled.` ); } const adminEntry = await this.#state.getAdminEntry(); if (!adminEntry) { throw new Error( - `Can not ban writer with address: ${address} - admin entry has not been initialized.` + `Can not ban writer with address: ${addresstToBan} - admin entry has not been initialized.` ); } - if (!isAddressValid(address, this.#config.addressPrefix)) { + if (!isAddressValid(addresstToBan, this.#config.addressPrefix)) { throw new Error( - `Can not ban writer with address: ${address} - invalid address.` + `Can not ban writer with address: ${addresstToBan} - invalid address.` ); } if (!this.#isAdmin(adminEntry)) { throw new Error( - `Can not ban writer with address: ${address} - You are not an admin.` + `Can not ban writer with address: ${addresstToBan} - You are not an admin.` ); } - const isWhitelisted = await this.#state.isAddressWhitelisted(address); - const nodeEntry = await this.#state.getNodeEntry(address); + const isWhitelisted = await this.#state.isAddressWhitelisted(addresstToBan); + const nodeEntry = await this.#state.getNodeEntry(addresstToBan); if (!isWhitelisted || null === nodeEntry || nodeEntry.isIndexer === true) { throw new Error( - `Can not ban writer with address: ${address} - node is not whitelisted or is an indexer.` + `Can not ban writer with address: ${addresstToBan} - node is not whitelisted or is an indexer.` ); } const txValidity = await this.#state.getIndexerSequenceState(); - const assembledBanValidatorMessage = await new CompleteStateMessageOperations(this.#wallet, this.#config) - .assembleBanWriterMessage( - address, - txValidity - ); - await this.#state.append(assembledBanValidatorMessage); + const assembledBanValidatorMessage = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteBanWriterMessage( + this.#wallet.address, + addresstToBan, + txValidity, + ) + const encodedPayload = safeEncodeApplyOperation(assembledBanValidatorMessage) + await this.#state.append(encodedPayload); } async #deployBootstrap(externalBootstrap, channel) { @@ -682,12 +667,15 @@ export class MainSettlementBus extends ReadyResource { } const txValidity = await this.#state.getIndexerSequenceState(); - const payload = await new PartialStateMessageOperations(this.#wallet, this.#config) - .assembleBootstrapDeploymentMessage( + + const payload = await applyStateMessageFactory(this.#wallet, this.#config) + .buildPartialBootstrapDeploymentMessage( + this.#wallet.address, externalBootstrap, channel, - txValidity.toString('hex') - ); + txValidity, + "json" + ) const success = await this.broadcastPartialTransaction(payload); @@ -703,31 +691,7 @@ export class MainSettlementBus extends ReadyResource { } - async #handleAddIndexerOperation(address) { - await this.#updateIndexerRole(address, true); - } - - async #handleRemoveIndexerOperation(address) { - await this.#updateIndexerRole(address, false); - } - - async #handleAddWriterOperation() { - await this.#requestWriterRole(true); - } - - async #handleRemoveWriterOperation() { - await this.#requestWriterRole(false); - } - - async #handleBanValidatorOperation(address) { - await this.#banValidator(address); - } - - async #handleBootstrapDeploymentOperation(bootstrapHex, channel) { - await this.#deployBootstrap(bootstrapHex, channel); - } - - async #handleTransferOperation(address, amount) { + async #handleTransferOperation(recipientAddress, amount) { if (!this.#config.enableWallet) { throw new Error( "Can not perform transfer - wallet is not enabled." @@ -740,7 +704,7 @@ export class MainSettlementBus extends ReadyResource { ); } - if (!isAddressValid(address, this.#config.addressPrefix)) { + if (!isAddressValid(recipientAddress, this.#config.addressPrefix)) { throw new Error("Invalid recipient address"); } @@ -765,7 +729,7 @@ export class MainSettlementBus extends ReadyResource { const fee = this.#state.getFee(); const feeBigInt = bufferToBigInt(fee); const senderBalance = bufferToBigInt(senderEntry.balance); - const isSelfTransfer = address === this.#wallet.address; + const isSelfTransfer = recipientAddress === this.#wallet.address; const totalDeductedAmount = isSelfTransfer ? feeBigInt : amountBigInt + feeBigInt; if (!(senderBalance >= totalDeductedAmount)) { @@ -773,13 +737,14 @@ export class MainSettlementBus extends ReadyResource { } const txValidity = await this.#state.getIndexerSequenceState(); - const payload = await new PartialStateMessageOperations(this.#wallet, this.#config) - .assembleTransferOperationMessage( - address, - amountBuffer.toString('hex'), - txValidity.toString('hex'), - ) - + const payload = await applyStateMessageFactory(this.#wallet, this.#config) + .buildPartialTransferOperationMessage( + this.#wallet.address, + recipientAddress, + amountBuffer, + txValidity, + "json" + ) const expectedNewBalance = senderBalance - totalDeductedAmount; console.info('Transfer Details:'); @@ -794,7 +759,6 @@ export class MainSettlementBus extends ReadyResource { console.info(`Total: ${bigIntToDecimalString(totalDeductedAmount)}`); } console.log(`Expected Balance After Transfer: ${bigIntToDecimalString(expectedNewBalance)}`); - const success = await this.broadcastPartialTransaction(payload); if (!success) { throw new Error("Failed to broadcast transfer transaction after multiple attempts."); @@ -804,7 +768,7 @@ export class MainSettlementBus extends ReadyResource { } - async #handleBalanceMigrationOperation() { + async #balanceMigrationOperation() { const isInitDisabled = await this.#state.isInitalizationDisabled() @@ -849,11 +813,18 @@ export class MainSettlementBus extends ReadyResource { await fileUtils.createMigrationEntryFile(addressBalancePair, migrationNumber); const txValidity = await this.#state.getIndexerSequenceState(); - const messages = await new CompleteStateMessageOperations(this.#wallet, this.#config) - .assembleBalanceInitializationMessages( + + let messages = []; + + for (const [recipientAddress, amountBuffer] of addressBalancePair) { + const payload = await applyStateMessageFactory(this.#wallet, this.#config).buildCompleteBalanceInitializationMessage( + this.#wallet.address, + recipientAddress, + amountBuffer, txValidity, - addressBalancePair, - ); + ) + messages.push(safeEncodeApplyOperation(payload)); + } console.log(`Total balance to migrate: ${bigIntToDecimalString(totalBalance)} across ${totalAddresses} addresses.`); @@ -903,6 +874,10 @@ export class MainSettlementBus extends ReadyResource { if (!this.#config.enableWallet) { throw new Error("Can not initialize an admin - wallet is not enabled."); } + const isInitDisabled = await this.#state.isInitalizationDisabled(); + if (isInitDisabled) { + throw new Error("Can not disable initialization - it is already disabled."); + } const adminEntry = await this.#state.getAdminEntry(); if (!adminEntry) { @@ -912,15 +887,24 @@ export class MainSettlementBus extends ReadyResource { if (!this.#isAdmin(adminEntry)) { throw new Error('Cannot perform whitelisting - you are not the admin!.'); } - // add more checks + if (!this.#wallet) { + throw new Error("Can not initialize an admin - wallet is not initialized."); + } + if (!this.#state.writingKey) { + throw new Error("Can not initialize an admin - writing key is not initialized."); + } const txValidity = await this.#state.getIndexerSequenceState(); - const payload = await new CompleteStateMessageOperations(this.#wallet, this.#config) - .assembleDisableInitializationMessage( + + + const payload = await applyStateMessageFactory(this.#wallet, this.#config) + .buildCompleteDisableInitializationMessage( + this.#wallet.address, this.#state.writingKey, txValidity, ) console.log('Disabling initialization...'); - await this.#state.append(payload); + const encodedPayload = safeEncodeApplyOperation(payload); + await this.#state.append(encodedPayload); } async interactiveMode() { @@ -951,26 +935,22 @@ export class MainSettlementBus extends ReadyResource { if (rl) rl.close(); await this.close(); }, - "/add_admin": () => this.#handleAdminCreation(), - "/add_admin --recovery": () => this.#handleAdminRecovery(), - "/add_whitelist": () => this.#handleWhitelistOperations(), - "/add_writer": () => this.#handleAddWriterOperation(), - "/remove_writer": () => this.#handleRemoveWriterOperation(), - "/core": () => coreInfoCommand(this.#state), - "/indexers_list": async () => { - console.log(await this.#state.getIndexersEntry()); - }, - "/validator_pool": () => { - this.#network.validatorConnectionManager.prettyPrint(); - }, - "/stats": () => verifyDag( + "/add_admin": async () => await this.#handleAdminCreation(), + "/add_admin --recovery": async () => await this.#handleAdminRecovery(), + "/add_whitelist": async () => await this.#handleWhitelistOperations(), + "/add_writer": async () => await this.#requestWriterRole(true), + "/remove_writer": async () => await this.#requestWriterRole(false), + "/core": async () => await coreInfoCommand(this.#state), + "/indexers_list": async () => console.log(await this.#state.getIndexersEntry()), + "/validator_pool": () => this.#network.validatorConnectionManager.prettyPrint(), + "/stats": async () => await verifyDag( this.#state, this.#network, this.#wallet, this.#state.writingKey ), - "/balance_migration": () => this.#handleBalanceMigrationOperation(), - "/disable_initialization": () => this.#disableInitialization() + "/balance_migration": async () => await this.#balanceMigrationOperation(), + "/disable_initialization": async () => await this.#disableInitialization() }; if (exactHandlers[command]) { @@ -988,20 +968,20 @@ export class MainSettlementBus extends ReadyResource { if (input.startsWith("/add_indexer")) { const address = parts[0]; - await this.#handleAddIndexerOperation(address); + await this.#updateWriterToIndexerRole(address, true); } else if (input.startsWith("/remove_indexer")) { const address = parts[0]; - await this.#handleRemoveIndexerOperation(address); + await this.#updateWriterToIndexerRole(address, false); } else if (input.startsWith("/ban_writer")) { const address = parts[0]; - await this.#handleBanValidatorOperation(address); + await this.#banValidator(address); } else if (input.startsWith("/deployment")) { const bootstrapToDeploy = parts[0]; const channel = parts[1] || randomBytes(32).toString("hex"); if (channel.length !== 64 || !isHexString(channel)) { throw new Error("Channel must be a 32-byte hex string"); } - await this.#handleBootstrapDeploymentOperation(bootstrapToDeploy, channel); + await this.#deployBootstrap(bootstrapToDeploy, channel); } else if (input.startsWith("/get_validator_addr")) { const wkHexString = parts[0]; await getValidatorAddressCommand(this.#state, wkHexString, this.#config.addressPrefix); @@ -1045,13 +1025,6 @@ export class MainSettlementBus extends ReadyResource { const result = getUnconfirmedLengthCommand(this.#state); if (rl) rl.prompt(); return result; - } else if (input.startsWith("/broadcast_transaction")) { - if (!payload) { - throw new Error("Transaction payload is required for broadcast_transaction command."); - } - const result = await this.broadcastTransactionCommand(payload); - if (rl) rl.prompt(); - return result; } else if (input.startsWith("/get_tx_payloads_bulk")) { if (!payload) { throw new Error("Missing payload for fetching tx payloads."); diff --git a/src/messages/base/StateBuilder.js b/src/messages/base/StateBuilder.js deleted file mode 100644 index ab950061..00000000 --- a/src/messages/base/StateBuilder.js +++ /dev/null @@ -1,25 +0,0 @@ -class StateBuilder { - - constructor() { - if (this.constructor === StateBuilder) { - throw new Error("Builder is an abstract class and cannot be instantiated directly."); - } - } - reset() { throw new Error("Method 'reset()' must be implemented.");} - forOperationType(operationType) {throw new Error("Method 'forOperationType()' must be implemented.");} - withAddress(address) { throw new Error("Method 'withAddress()' must be implemented.");} - withWriterKey(writerKey) { throw new Error("Method 'withWriterKey()' must be implemented.");} - async buildValueAndSign() { throw new Error("Method 'buildValueAndSign()' must be implemented.");} - withIncomingAddress(address) { throw new Error("Method 'withIncomingAddress()' must be implemented.");} - withIncomingWriterKey(writerKey) { throw new Error("Method 'withIncomingWriterKey()' must be implemented.");} - withIncomingNonce(nonce) { throw new Error("Method 'withIncomingNonce()' must be implemented.");} - withContentHash(contentHash) { throw new Error("Method 'withContentHash()' must be implemented.");} - withIncomingSignature(signature) { throw new Error("Method 'withIncomingSignature()' must be implemented.");} - withExternalBootstrap(bootstrapKey) { throw new Error("Method 'withExternalBootstrap()' must be implemented.");} - withMsbBootstrap(msbBootstrap) { throw new Error("Method 'withMsbBootstrap()' must be implemented.");} - withTxHash(txHash) { throw new Error("Method 'withTxHash()' must be implemented.");} - withTxValidity(txValidity) { throw new Error("Method 'withTxValidity()' must be implemented.");} - withAmount(amount) { throw new Error("Method 'withAmount()' must be implemented.");} -} - -export default StateBuilder; diff --git a/src/messages/completeStateMessages/CompleteStateMessageBuilder.js b/src/messages/completeStateMessages/CompleteStateMessageBuilder.js deleted file mode 100644 index 14e1a109..00000000 --- a/src/messages/completeStateMessages/CompleteStateMessageBuilder.js +++ /dev/null @@ -1,425 +0,0 @@ -import b4a from 'b4a'; -import PeerWallet from 'trac-wallet'; - -import StateBuilder from '../base/StateBuilder.js' -import {createMessage} from '../../utils/buffer.js'; -import {OperationType} from '../../utils/protobuf/applyOperations.cjs' -import {addressToBuffer, bufferToAddress} from '../../core/state/utils/address.js'; -import {isAddressValid} from "../../core/state/utils/address.js"; -import { - isCoreAdmin, - isAdminControl, - isRoleAccess, - isTransaction, - isBootstrapDeployment, - isTransfer, - isBalanceInitialization -} from '../../utils/operations.js'; - -class CompleteStateMessageBuilder extends StateBuilder { - #wallet; - #config - #operationType; - #address; - #writingKey; - #payload; - #txHash; - #incomingAddress; - #incomingWriterKey; - #incomingNonce; - #contentHash; - #incomingSignature; - #externalBootstrap; - #channel; - #msbBootstrap; - #validatorNonce; - #txValidity; - #amount; - - /** - * - * @param {PeerWallet} wallet - * @param {Config} config - */ - constructor(wallet, config) { - super(); - this.#config = config; - if (!wallet || typeof wallet !== 'object') { - throw new Error('Wallet must be a valid wallet object'); - } - if (!isAddressValid(wallet.address, this.#config.addressPrefix)) { - throw new Error('Wallet should have a valid TRAC address.'); - } - - this.#wallet = wallet; - this.reset(); - } - - reset() { - this.#operationType = OperationType.UNKNOWN; - this.#address = null; - this.#writingKey = null; - this.#payload = {}; - this.#txHash = null; - this.#incomingAddress = null; - this.#incomingWriterKey = null; - this.#incomingNonce = null; - this.#contentHash = null; - this.#incomingSignature = null; - this.#externalBootstrap = null; - this.#channel = null; - this.#msbBootstrap = null; - this.#validatorNonce = null; - this.#txValidity = null; - this.#amount = null; - } - - forOperationType(operationType) { - if (!Object.values(OperationType).includes(operationType) || OperationType === OperationType.UNKNOWN) { - throw new Error(`Invalid operation type: ${operationType}`); - } - this.#operationType = operationType; - this.#payload.type = operationType; - return this; - } - - withAddress(address) { - if (b4a.isBuffer(address) && address.length === this.#config.addressLength) { - address = bufferToAddress(address, this.#config.addressPrefix); - } - - if (!isAddressValid(address, this.#config.addressPrefix)) { - throw new Error(`Address field must be a valid TRAC bech32m address with length ${this.#config.addressLength}.`); - } - - this.#address = addressToBuffer(address, this.#config.addressPrefix); - this.#payload.address = this.#address; - return this; - } - - withWriterKey(writingKey) { - if (!b4a.isBuffer(writingKey) || writingKey.length !== 32) { - throw new Error('Writer key must be a 32 length buffer.'); - } - this.#writingKey = writingKey; - return this; - } - - withTxHash(txHash) { - if (!b4a.isBuffer(txHash) || txHash.length !== 32) { - throw new Error('Transaction hash must be a 32-byte buffer.'); - } - this.#txHash = txHash; - return this; - } - - withIncomingAddress(address) { - if (b4a.isBuffer(address) && address.length === this.#config.addressLength) { - address = bufferToAddress(address, this.#config.addressPrefix); - } - - if (!isAddressValid(address, this.#config.addressPrefix)) { - throw new Error(`Address field must be a valid TRAC bech32m address with length ${this.#config.addressLength}.`); - } - - this.#incomingAddress = addressToBuffer(address, this.#config.addressPrefix); - return this; - } - - withIncomingWriterKey(writerKey) { - if (!b4a.isBuffer(writerKey) || writerKey.length !== 32) { - throw new Error('Incoming writer key must be a 32-byte buffer.'); - } - this.#incomingWriterKey = writerKey; - return this; - } - - withIncomingNonce(nonce) { - if (!b4a.isBuffer(nonce) || nonce.length !== 32) { - throw new Error('Incoming nonce must be a 32-byte buffer.'); - } - this.#incomingNonce = nonce; - return this; - } - - withContentHash(contentHash) { - if (!b4a.isBuffer(contentHash) || contentHash.length !== 32) { - throw new Error('Content hash must be a 32-byte buffer.'); - } - this.#contentHash = contentHash; - return this; - } - - withIncomingSignature(signature) { - if (!b4a.isBuffer(signature) || signature.length !== 64) { - throw new Error('Incoming signature must be a 64-byte buffer.'); - } - this.#incomingSignature = signature; - return this; - } - - withExternalBootstrap(bootstrapKey) { - if (!b4a.isBuffer(bootstrapKey) || bootstrapKey.length !== 32) { - throw new Error('Bootstrap key must be a 32-byte buffer.'); - } - this.#externalBootstrap = bootstrapKey; - return this; - } - - withMsbBootstrap(msbBootstrap) { - if (!b4a.isBuffer(msbBootstrap) || msbBootstrap.length !== 32) { - throw new Error('MSB bootstrap must be a 32-byte buffer.'); - } - this.#msbBootstrap = msbBootstrap; - return this; - } - - withChannel(channel) { - if (!b4a.isBuffer(channel) || channel.length !== 32) { - throw new Error('Channel must be a 32-byte buffer.'); - } - this.#channel = channel; - return this; - } - - withTxValidity(txValidity) { - if (!b4a.isBuffer(txValidity) || txValidity.length !== 32) { - throw new Error('Transaction validity must be a 32-byte buffer.'); - } - this.#txValidity = txValidity; - return this; - } - - withAmount(amount) { - if (!b4a.isBuffer(amount) || amount.length !== 16) { - throw new Error('Amount must be a 16-byte buffer.'); - } - - this.#amount = amount; - return this; - } - - async buildValueAndSign() { - if (!this.#operationType || !this.#address) { - throw new Error('Operation type, address must be set before building the message.'); - } - - if (this.#operationType === OperationType.UNKNOWN) { - throw new Error('UNKNOWN is not allowed to construct'); - } - - const nonce = PeerWallet.generateNonce(); - - let msg = null; - let tx = null; - let signature = null; - - // all incoming data from setters should be as buffer data type, createMessage accept only buffer and uint32 - switch (this.#operationType) { - // Complete by default - case OperationType.ADD_ADMIN: - case OperationType.DISABLE_INITIALIZATION: - msg = createMessage( - this.#config.networkId, - this.#txValidity, - this.#writingKey, - nonce, - this.#operationType); - break; - // Complete by default - case OperationType.BALANCE_INITIALIZATION: - if (!this.#incomingAddress || !this.#amount || !this.#txValidity || !this.#address) { - throw new Error('All balance initialization fields must be set before building the message!'); - } - msg = createMessage( - this.#config.networkId, - this.#txValidity, - this.#incomingAddress, - this.#amount, - nonce, - this.#operationType - ); - break; - // Partial need to be signed - case OperationType.ADD_WRITER: - case OperationType.REMOVE_WRITER: - case OperationType.ADMIN_RECOVERY: - msg = createMessage( - this.#config.networkId, - this.#txHash, - nonce, - this.#operationType - ); - break; - // Complete by default - case OperationType.APPEND_WHITELIST: - case OperationType.ADD_INDEXER: - case OperationType.REMOVE_INDEXER: - case OperationType.BAN_VALIDATOR: - if (this.#wallet.address === bufferToAddress(this.#incomingAddress, this.#config.addressPrefix)) { - throw new Error('Address must not be the same as the wallet address for basic operations.'); - } - - msg = createMessage( - this.#config.networkId, - this.#txValidity, - this.#incomingAddress, - nonce, - this.#operationType - ); - - break; - // Partial need to be signed - case OperationType.BOOTSTRAP_DEPLOYMENT: - if (!this.#txHash || !this.#externalBootstrap || !this.#channel || !this.#incomingNonce || !this.#incomingSignature) { - throw new Error('All bootstrap deployment fields must be set before building the message!'); - } - msg = createMessage( - this.#config.networkId, - this.#txHash, - nonce, - this.#operationType - ); - break; - - // Partial need to be signed - case OperationType.TX: - if (!this.#txHash || !this.#txValidity || !this.#address || !this.#incomingWriterKey || - !this.#incomingNonce || !this.#contentHash || !this.#incomingSignature || - !this.#externalBootstrap || !this.#msbBootstrap) { - throw new Error('All postTx fields must be set before building the message!'); - } - msg = createMessage( - this.#config.networkId, - this.#txHash, - nonce, - this.#operationType - ); - break; - - case OperationType.TRANSFER: - if (!this.#txHash || !this.#txValidity || !this.#address || !this.#incomingNonce || - !this.#incomingSignature || !this.#amount || !this.#incomingAddress) { - throw new Error('All transfer fields must be set before building the message!'); - } - msg = createMessage( - this.#config.networkId, - this.#txHash, - nonce, - this.#operationType - ); - break; - - default: - throw new Error(`Unsupported operation type for building value: ${OperationType[this.#operationType]}.`); - } - - tx = await PeerWallet.blake3(msg); - signature = this.#wallet.sign(tx); - - if (isCoreAdmin(this.#operationType)) { - this.#payload.cao = { - tx: tx, - txv: this.#txValidity, - iw: this.#writingKey, - in: nonce, - is: signature - }; - } else if (isAdminControl(this.#operationType)) { - this.#payload.aco = { - tx: tx, - txv: this.#txValidity, - ia: this.#incomingAddress, - in: nonce, - is: signature - }; - } - else if (isRoleAccess(this.#operationType)) { - this.#payload.rao = { - tx: this.#txHash, - txv: this.#txValidity, - iw: this.#incomingWriterKey, - in: this.#incomingNonce, - is: this.#incomingSignature, - va: addressToBuffer(this.#wallet.address, this.#config.addressPrefix), - vn: nonce, - vs: signature, - }; - } else if (isTransaction(this.#operationType)) { - this.#payload.txo = { - tx: this.#txHash, - txv: this.#txValidity, - iw: this.#incomingWriterKey, - ch: this.#contentHash, - bs: this.#externalBootstrap, - mbs: this.#msbBootstrap, - in: this.#incomingNonce, - is: this.#incomingSignature, - va: addressToBuffer(this.#wallet.address, this.#config.addressPrefix), - vn: nonce, - vs: signature, - }; - } else if (isBootstrapDeployment(this.#operationType)) { - this.#payload.bdo = { - tx: this.#txHash, - txv: this.#txValidity, - bs: this.#externalBootstrap, - ic: this.#channel, - in: this.#incomingNonce, - is: this.#incomingSignature, - va: addressToBuffer(this.#wallet.address, this.#config.addressPrefix), - vn: nonce, - vs: signature - } - } else if (isTransfer(this.#operationType)) { - this.#payload.tro = { - tx: this.#txHash, - txv: this.#txValidity, - to: this.#incomingAddress, - am: this.#amount, - in: this.#incomingNonce, - is: this.#incomingSignature, - va: addressToBuffer(this.#wallet.address, this.#config.addressPrefix), - vn: nonce, - vs: signature - } - } else if (isBalanceInitialization(this.#operationType)) { - this.#payload.bio = { - tx: tx, - txv: this.#txValidity, - ia: this.#incomingAddress, - am: this.#amount, - in: nonce, - is: signature - } - } - else { - throw new Error(`No corresponding value type for operation: ${OperationType[this.#operationType]}.`); - } - - return this; - } - - getPayload() { - if ( - !this.#payload.type || - !this.#payload.address || - ( - !this.#payload.cao && - !this.#payload.aco && - !this.#payload.rao && - !this.#payload.txo && - !this.#payload.bdo && - !this.#payload.tro && - !this.#payload.bio - ) - ) { - throw new Error('Product is not fully assembled. Missing type, address, or value (cao/aco/rao/txo/bdo/tro/bio).'); - } - const res = this.#payload; - this.reset(); - return res; - } -} - -export default CompleteStateMessageBuilder; diff --git a/src/messages/completeStateMessages/CompleteStateMessageDirector.js b/src/messages/completeStateMessages/CompleteStateMessageDirector.js deleted file mode 100644 index 1928078f..00000000 --- a/src/messages/completeStateMessages/CompleteStateMessageDirector.js +++ /dev/null @@ -1,252 +0,0 @@ -import StateBuilder from '../base/StateBuilder.js' -import { OperationType } from '../../utils/protobuf/applyOperations.cjs' - -class CompleteStateMessageDirector { - #builder; - - set builder(builderInstance) { - if (!(builderInstance instanceof StateBuilder)) { - throw new Error('Director requires a Builder instance.'); - } - this.#builder = builderInstance; - } - - async buildAddAdminMessage(invokerAddress, writingKey, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.ADD_ADMIN) - .withAddress(invokerAddress) - .withWriterKey(writingKey) - .withTxValidity(txValidity) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildDisableInitializationMessage(invokerAddress, writingKey, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.DISABLE_INITIALIZATION) - .withAddress(invokerAddress) - .withWriterKey(writingKey) - .withTxValidity(txValidity) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildBalanceInitializationMessage(invokerAddress, recipientAddress, amount, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - await this.#builder - .forOperationType(OperationType.BALANCE_INITIALIZATION) - .withAddress(invokerAddress) - .withIncomingAddress(recipientAddress) - .withAmount(amount) - .withTxValidity(txValidity) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildAppendWhitelistMessage(invokerAddress, incomingAddress, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.APPEND_WHITELIST) - .withAddress(invokerAddress) - .withTxValidity(txValidity) - .withIncomingAddress(incomingAddress) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildAddWriterMessage( - invokerAddress, - txHash, - txValidity, - incomingWritingKey, - incomingNonce, - incomingSignature - ) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.ADD_WRITER) - .withAddress(invokerAddress) - .withTxHash(txHash) - .withTxValidity(txValidity) - .withIncomingWriterKey(incomingWritingKey) - .withIncomingNonce(incomingNonce) - .withIncomingSignature(incomingSignature) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildRemoveWriterMessage( - invokerAddress, - txHash, - txValidity, - incomingWritingKey, - incomingNonce, - incomingSignature - ) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.REMOVE_WRITER) - .withAddress(invokerAddress) - .withTxHash(txHash) - .withTxValidity(txValidity) - .withIncomingWriterKey(incomingWritingKey) - .withIncomingNonce(incomingNonce) - .withIncomingSignature(incomingSignature) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildAdminRecoveryMessage( - invokerAddress, - txHash, - txValidity, - incomingWritingKey, - incomingNonce, - incomingSignature - ) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.ADMIN_RECOVERY) - .withAddress(invokerAddress) - .withTxHash(txHash) - .withTxValidity(txValidity) - .withIncomingWriterKey(incomingWritingKey) - .withIncomingNonce(incomingNonce) - .withIncomingSignature(incomingSignature) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildAddIndexerMessage(invokerAddress, incomingAddress, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.ADD_INDEXER) - .withAddress(invokerAddress) - .withTxValidity(txValidity) - .withIncomingAddress(incomingAddress) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildRemoveIndexerMessage(invokerAddress, incomingAddress, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - await this.#builder - .forOperationType(OperationType.REMOVE_INDEXER) - .withAddress(invokerAddress) - .withTxValidity(txValidity) - .withIncomingAddress(incomingAddress) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildBanWriterMessage(invokerAddress, incomingAddress, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.BAN_VALIDATOR) - .withAddress(invokerAddress) - .withTxValidity(txValidity) - .withIncomingAddress(incomingAddress) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildTransactionOperationMessage( - invokerAddress, - txHash, - txValidity, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap, - ) { - if (!this.#builder) throw new Error('Builder has not been set.'); - await this.#builder - .forOperationType(OperationType.TX) - .withAddress(invokerAddress) - .withTxHash(txHash) - .withTxValidity(txValidity) - .withIncomingWriterKey(incomingWriterKey) - .withIncomingNonce(incomingNonce) - .withContentHash(contentHash) - .withIncomingSignature(incomingSignature) - .withExternalBootstrap(externalBootstrap) - .withMsbBootstrap(msbBootstrap) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildBootstrapDeploymentMessage( - invokerAddress, - transactionHash, - txValidity, - externalBootstrap, - channel, - incomingNonce, - incomingSignature - ) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.BOOTSTRAP_DEPLOYMENT) - .withAddress(invokerAddress) - .withTxHash(transactionHash) - .withTxValidity(txValidity) - .withExternalBootstrap(externalBootstrap) - .withChannel(channel) - .withIncomingNonce(incomingNonce) - .withIncomingSignature(incomingSignature) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildTransferOperationMessage( - invokerAddress, - transactionHash, - txValidity, - incomingNonce, - recipientAddress, - amount, - incomingSignature - ) { - if (!this.#builder) throw new Error('Builder has not been set.'); - await this.#builder - .forOperationType(OperationType.TRANSFER) - .withAddress(invokerAddress) - .withTxHash(transactionHash) - .withTxValidity(txValidity) - .withIncomingNonce(incomingNonce) - .withIncomingAddress(recipientAddress) - .withAmount(amount) - .withIncomingSignature(incomingSignature) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - -} - -export default CompleteStateMessageDirector; diff --git a/src/messages/completeStateMessages/CompleteStateMessageOperations.js b/src/messages/completeStateMessages/CompleteStateMessageOperations.js deleted file mode 100644 index c17801eb..00000000 --- a/src/messages/completeStateMessages/CompleteStateMessageOperations.js +++ /dev/null @@ -1,296 +0,0 @@ -import CompleteStateMessageDirector from './CompleteStateMessageDirector.js'; -import CompleteStateMessageBuilder from './CompleteStateMessageBuilder.js'; -import { safeEncodeApplyOperation } from '../../utils/protobuf/operationHelpers.js'; - -class CompleteStateMessageOperations { - #config - #wallet - constructor(wallet, config) { - this.#wallet = wallet - this.#config = config - } - - async assembleAddAdminMessage(writingKey, txValidity) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildAddAdminMessage(this.#wallet.address, writingKey, txValidity); - return safeEncodeApplyOperation(payload); - } catch (error) { - throw new Error(`Failed to assemble admin message: ${error.message}`); - } - } - - async assembleDisableInitializationMessage(writingKey, txValidity) { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildDisableInitializationMessage(this.#wallet.address, writingKey, txValidity); - return safeEncodeApplyOperation(payload); - } - - async assembleAddWriterMessage( - invokerAddress, - transactionHash, - txValidity, - incomingWritingKey, - incomingNonce, - incomingSignature - ) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildAddWriterMessage( - invokerAddress, - transactionHash, - txValidity, - incomingWritingKey, - incomingNonce, - incomingSignature - ); - return safeEncodeApplyOperation(payload); - - } catch (error) { - throw new Error(`Failed to assemble add writer message: ${error.message}`); - } - } - - async assembleRemoveWriterMessage( - invokerAddress, - transactionHash, - txValidity, - incomingWritingKey, - incomingNonce, - incomingSignature - ) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildRemoveWriterMessage( - invokerAddress, - transactionHash, - txValidity, - incomingWritingKey, - incomingNonce, - incomingSignature - ); - return safeEncodeApplyOperation(payload); - - } catch (error) { - throw new Error(`Failed to assemble remove writer message: ${error.message}`); - } - } - - async assembleAdminRecoveryMessage( - invokerAddress, - transactionHash, - txValidity, - incomingWritingKey, - incomingNonce, - incomingSignature - ) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildAdminRecoveryMessage( - invokerAddress, - transactionHash, - txValidity, - incomingWritingKey, - incomingNonce, - incomingSignature - ); - return safeEncodeApplyOperation(payload); - - } catch (error) { - throw new Error(`Failed to assemble remove writer message: ${error.message}`); - } - } - - async assembleAddIndexerMessage(incomingAddress, txValidity) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildAddIndexerMessage(this.#wallet.address, incomingAddress, txValidity); - return safeEncodeApplyOperation(payload); - - } catch (error) { - throw new Error(`Failed to assemble addIndexerMessage: ${error.message}`); - } - } - - - - async assembleRemoveIndexerMessage(incomingAddress, txValidity) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildRemoveIndexerMessage(this.#wallet.address, incomingAddress, txValidity); - return safeEncodeApplyOperation(payload); - - } catch (error) { - throw new Error(`Failed to assemble removeIndexerMessage: ${error.message}`); - } - } - - async assembleAppendWhitelistMessages(txValidity, addressToWhitelist) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildAppendWhitelistMessage(this.#wallet.address, addressToWhitelist, txValidity); - - return safeEncodeApplyOperation(payload);; - } catch (error) { - throw new Error(`Failed to assemble appendWhitelistMessages: ${error.message}`); - } - } - - async assembleBalanceInitializationMessages(txValidity, addressBalancePair) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const messages = []; - - for (const [recipientAddress, balanceBuffer] of addressBalancePair) { - const payload = await director.buildBalanceInitializationMessage( - this.#wallet.address, - recipientAddress, - balanceBuffer, - txValidity - ); - messages.push(safeEncodeApplyOperation(payload)); - } - return messages; - - } catch (error) { - throw new Error(`Failed to assemble balance initialization messages: ${error.message}`); - } - } - - async assembleBanWriterMessage(incomingAddress, txValidity) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildBanWriterMessage(this.#wallet.address, incomingAddress, txValidity); - return safeEncodeApplyOperation(payload); - - } catch (error) { - throw new Error(`Failed to assemble ban writer message: ${error.message}`); - } - } - - async assembleCompleteTransactionOperationMessage( - invokerAddress, - txHash, - txValidity, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - const payload = await director.buildTransactionOperationMessage( - invokerAddress, - txHash, - txValidity, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap, - ); - return safeEncodeApplyOperation(payload); - - } catch (error) { - throw new Error(`Failed to assemble transaction Operation: ${error.message}`); - } - } - - async assembleCompleteBootstrapDeployment( - invokerAddress, - transactionHash, - txValidity, - externalBootstrap, - channel, - incomingNonce, - incomingSignature - ) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildBootstrapDeploymentMessage( - invokerAddress, - transactionHash, - txValidity, - externalBootstrap, - channel, - incomingNonce, - incomingSignature, - ); - return safeEncodeApplyOperation(payload); - - } catch (error) { - throw new Error(`Failed to assemble bootstrap deployment message: ${error.message}`); - } - } - - async assembleCompleteTransferOperationMessage( - invokerAddress, - transactionHash, - txValidity, - incomingNonce, - recipientAddress, - amount, - incomingSignature - ) { - try { - const builder = new CompleteStateMessageBuilder(this.#wallet, this.#config); - const director = new CompleteStateMessageDirector(); - director.builder = builder; - - const payload = await director.buildTransferOperationMessage( - invokerAddress, - transactionHash, - txValidity, - incomingNonce, - recipientAddress, - amount, - incomingSignature - ); - return safeEncodeApplyOperation(payload); - - } catch (error) { - throw new Error(`Failed to assemble transfer operation message: ${error.message}`); - } - } - -} - -export default CompleteStateMessageOperations; diff --git a/src/messages/network/v1/NetworkMessageBuilder.js b/src/messages/network/v1/NetworkMessageBuilder.js new file mode 100644 index 00000000..4c2e7413 --- /dev/null +++ b/src/messages/network/v1/NetworkMessageBuilder.js @@ -0,0 +1,325 @@ +import PeerWallet from 'trac-wallet'; +import b4a from 'b4a'; +import {createMessage, safeWriteUInt32BE, sessionIdToBuffer, timestampToBuffer} from "../../../utils/buffer.js"; +import {NetworkOperationType, ResultCode} from '../../../utils/constants.js'; +import {addressToBuffer, isAddressValid} from "../../../core/state/utils/address.js"; +import {encodeCapabilities} from "../../../utils/buffer.js"; + +/** + * Builder for v1 internal network protocol messages. + * @param {PeerWallet} wallet + * @param {object} config + */ +class NetworkMessageBuilder { + #wallet; + #type; + #capabilities; + #sessionId; + #timestamp; + #issuerAddress; + #resultCode; + #data; + #header; + #payloadKey; + #body; + #config; + + /** + * @param {PeerWallet} wallet + * @param {object} config + */ + constructor(wallet, config) { + this.#config = config; + if (!wallet || typeof wallet !== 'object') { + throw new Error('Wallet must be a valid wallet object'); + } + if (!isAddressValid(wallet.address, this.#config.addressPrefix)) { + throw new Error('Wallet should have a valid TRAC address.'); + } + + this.#wallet = wallet; + } + + setSessionId(sessionId) { + this.#sessionId = sessionId; + return this; + } + + setTimestamp() { + this.#timestamp = Date.now(); + return this; + } + + setIssuerAddress(issuerAddress) { + if (!isAddressValid(issuerAddress, this.#config.addressPrefix)) { + throw new Error('Issuer TRAC address must be valid.'); + } + this.#issuerAddress = issuerAddress; + return this; + } + + setCapabilities(capabilities = []) { + if (!Array.isArray(capabilities) || !capabilities.every(capability => typeof capability === 'string')) { + throw new Error('Capabilities must be a string array.'); + } + + this.#capabilities = capabilities + return this; + } + + setType(type) { + if (!Object.values(NetworkOperationType).includes(type)) { + throw new Error(`Invalid operation type: ${type}`); + } + this.#type = type; + return this; + } + + setResultCode(code) { + if (!Object.values(ResultCode).includes(code)) { + throw new Error(`Invalid network result code: ${code}`); + } + + this.#resultCode = code; + return this; + } + + setData(data) { + if (!b4a.isBuffer(data)) { + throw new Error(`Data must be a buffer.`); + } + this.#data = data; + return this; + } + + #setHeader() { + if (!this.#type) throw new Error('Header requires type to be set'); + if (!this.#sessionId) throw new Error('Header requires session_id to be set'); + if (!this.#timestamp) throw new Error('Header requires a timestamp provider'); + if (!Array.isArray(this.#capabilities)) throw new Error('Header requires capabilities array'); + + this.#header = { + type: this.#type, + session_id: this.#sessionId, + timestamp: this.#timestamp, + capabilities: this.#capabilities, + }; + return this; + } + + async #buildValidatorConnectionRequestPayload() { + const issuer = this.#issuerAddress + if (!isAddressValid(issuer, this.#config.addressPrefix)) { + throw new Error('Issuer address must be a valid TRAC address'); + } + + if (this.#issuerAddress !== this.#wallet.address) { + throw new Error('Issuer address must be the signer address'); + } + + const nonce = PeerWallet.generateNonce(); + const tsBuf = timestampToBuffer(this.#timestamp); + const sessionBuf = sessionIdToBuffer(this.#sessionId); + const message = createMessage( + this.#type, + sessionBuf, + tsBuf, + addressToBuffer(issuer, this.#config.addressPrefix), + nonce, + encodeCapabilities(this.#capabilities), + ); + const hash = await PeerWallet.blake3(message); + const signature = this.#wallet.sign(hash); + + this.#payloadKey = 'validator_connection_request'; + this.#body = { + issuer_address: issuer, + nonce, + signature + }; + } + + async #buildValidatorConnectionResponsePayload() { + const issuer = this.#issuerAddress + if (!isAddressValid(issuer, this.#config.addressPrefix)) { + throw new Error('Issuer address must be a valid TRAC address'); + } + + if (this.#issuerAddress === this.#wallet.address) { + throw new Error('Issuer address must be the different than the signer address'); + } + + if (this.#resultCode === null || this.#resultCode === undefined) { + throw new Error('Result code must be set before building validator connection response'); + } + + const nonce = PeerWallet.generateNonce(); + const tsBuf = timestampToBuffer(this.#timestamp); + const sessionBuf = sessionIdToBuffer(this.#sessionId); + const message = createMessage( + this.#type, + sessionBuf, + tsBuf, + addressToBuffer(issuer, this.#config.addressPrefix), + nonce, + safeWriteUInt32BE(this.#resultCode, 0), + encodeCapabilities(this.#capabilities), + ); + const hash = await PeerWallet.blake3(message); + const signature = this.#wallet.sign(hash); + + this.#payloadKey = 'validator_connection_response'; + this.#body = { + issuer_address: issuer, + nonce, + signature, + result: this.#resultCode + }; + } + + async #buildLivenessRequestPayload() { + const nonce = PeerWallet.generateNonce(); + const tsBuf = timestampToBuffer(this.#timestamp); + const sessionBuf = sessionIdToBuffer(this.#sessionId); + const message = createMessage( + this.#type, + sessionBuf, + tsBuf, + nonce, + encodeCapabilities(this.#capabilities), + ); + const hash = await PeerWallet.blake3(message); + const signature = this.#wallet.sign(hash); + + this.#payloadKey = 'liveness_request'; + this.#body = { + nonce, + signature + }; + } + + async #buildLivenessResponsePayload() { + if (this.#resultCode === null || this.#resultCode === undefined) { + throw new Error('Result code must be set before building liveness response'); + } + + const nonce = PeerWallet.generateNonce(); + const tsBuf = timestampToBuffer(this.#timestamp); + const sessionBuf = sessionIdToBuffer(this.#sessionId); + const message = createMessage( + this.#type, + sessionBuf, + tsBuf, + nonce, + safeWriteUInt32BE(this.#resultCode, 0), + encodeCapabilities(this.#capabilities), + ); + const hash = await PeerWallet.blake3(message); + const signature = this.#wallet.sign(hash); + + this.#payloadKey = 'liveness_response'; + this.#body = { + nonce, + signature, + result: this.#resultCode + }; + } + + async #buildBroadcastRequestPayload() { + if (!b4a.isBuffer(this.#data)) { + throw new Error('Data must be set before building broadcast transaction request'); + } + const nonce = PeerWallet.generateNonce(); + const tsBuf = timestampToBuffer(this.#timestamp); + const sessionBuf = sessionIdToBuffer(this.#sessionId); + const message = createMessage( + this.#type, + sessionBuf, + tsBuf, + this.#data, + nonce, + encodeCapabilities(this.#capabilities), + ); + const hash = await PeerWallet.blake3(message); + const signature = this.#wallet.sign(hash); + + this.#payloadKey = 'broadcast_transaction_request'; + this.#body = { + data: this.#data, + nonce, + signature + }; + } + + async #buildBroadcastTransactionResponse() { + if (this.#resultCode === null || this.#resultCode === undefined) { + throw new Error('Result code must be set before building broadcast transaction response'); + } + const nonce = PeerWallet.generateNonce(); + const tsBuf = timestampToBuffer(this.#timestamp); + const sessionBuf = sessionIdToBuffer(this.#sessionId); + const message = createMessage( + this.#type, + sessionBuf, + tsBuf, + nonce, + safeWriteUInt32BE(this.#resultCode, 0), + encodeCapabilities(this.#capabilities), + ); + const hash = await PeerWallet.blake3(message); + const signature = this.#wallet.sign(hash); + + this.#payloadKey = 'broadcast_transaction_response'; + this.#body = { + nonce, + signature, + result: this.#resultCode + }; + } + + async buildPayload() { + this.#setHeader(); + + switch (this.#type) { + case NetworkOperationType.VALIDATOR_CONNECTION_REQUEST: { + await this.#buildValidatorConnectionRequestPayload(); + break; + } + case NetworkOperationType.VALIDATOR_CONNECTION_RESPONSE: { + await this.#buildValidatorConnectionResponsePayload(); + break; + } + case NetworkOperationType.LIVENESS_REQUEST: { + await this.#buildLivenessRequestPayload(); + break; + } + case NetworkOperationType.LIVENESS_RESPONSE: { + await this.#buildLivenessResponsePayload(); + break; + } + case NetworkOperationType.BROADCAST_TRANSACTION_REQUEST: { + await this.#buildBroadcastRequestPayload(); + break; + } + case NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE: { + await this.#buildBroadcastTransactionResponse(); + break; + } + default: + throw new Error(`Unsupported network type ${this.#type}`); + } + } + + getResult() { + if (!this.#header || !this.#payloadKey || !this.#body) { + throw new Error('Header or payload not set before getResult'); + } + + return { + ...this.#header, + [this.#payloadKey]: this.#body + }; + } +} + +export default NetworkMessageBuilder; diff --git a/src/messages/network/v1/NetworkMessageDirector.js b/src/messages/network/v1/NetworkMessageDirector.js new file mode 100644 index 00000000..beaa41f1 --- /dev/null +++ b/src/messages/network/v1/NetworkMessageDirector.js @@ -0,0 +1,137 @@ +import { NetworkOperationType } from '../../../utils/constants.js'; + +/** + * Director for v1 internal network protocol messages. + */ +class NetworkMessageDirector { + #builder; + + /** + * @param {NetworkMessageBuilder} builderInstance + */ + constructor(builderInstance) { + this.#builder = builderInstance; + } + + /** + * Build a validator connection request message. + * @param {number} sessionId + * @param {string} issuerAddress + * @param {string[]} capabilities + * @returns {Promise} + */ + async buildValidatorConnectionRequest(sessionId, issuerAddress, capabilities) { + await this.#builder + .setType(NetworkOperationType.VALIDATOR_CONNECTION_REQUEST) + .setSessionId(sessionId) + .setTimestamp() + .setIssuerAddress(issuerAddress) + .setCapabilities(capabilities) + .buildPayload() + + + return this.#builder.getResult(); + } + + /** + * Build a validator connection response message. + * @param {number} sessionId + * @param {string} issuerAddress + * @param {string[]} capabilities + * @param {number} statusCode + * @returns {Promise} + */ + async buildValidatorConnectionResponse(sessionId, issuerAddress, capabilities, statusCode) { + await this.#builder + .setType(NetworkOperationType.VALIDATOR_CONNECTION_RESPONSE) + .setSessionId(sessionId) + .setTimestamp() + .setIssuerAddress(issuerAddress) + .setCapabilities(capabilities) + .setResultCode(statusCode) + .buildPayload() + + return this.#builder.getResult(); + } + + /** + * Build a liveness request message. + * @param {number} sessionId + * @param {Buffer} data + * @param {string[]} capabilities + * @returns {Promise} + */ + async buildLivenessRequest(sessionId, data, capabilities) { + await this.#builder + .setType(NetworkOperationType.LIVENESS_REQUEST) + .setSessionId(sessionId) + .setTimestamp() + .setData(data) + .setCapabilities(capabilities) + .buildPayload(); + + return this.#builder.getResult(); + } + + /** + * Build a liveness response message. + * @param {number} sessionId + * @param {Buffer} data + * @param {string[]} capabilities + * @param {number} statusCode + * @returns {Promise} + */ + async buildLivenessResponse(sessionId, data, capabilities, statusCode) { + await this.#builder + .setType(NetworkOperationType.LIVENESS_RESPONSE) + .setSessionId(sessionId) + .setTimestamp() + .setData(data) + .setCapabilities(capabilities) + .setResultCode(statusCode) + .buildPayload(); + + return this.#builder.getResult(); + } + + /** + * Build a broadcast transaction request message. + * @param {number} sessionId + * @param {Buffer} data + * @param {string[]} capabilities + * @returns {Promise} + */ + async buildBroadcastTransactionRequest(sessionId, data, capabilities) { + await this.#builder + .setType(NetworkOperationType.BROADCAST_TRANSACTION_REQUEST) + .setSessionId(sessionId) + .setTimestamp() + .setData(data) + .setCapabilities(capabilities) + .buildPayload(); + + return this.#builder.getResult(); + } + + /** + * Build a broadcast transaction response message. + * @param {number} sessionId + * @param {string[]} capabilities + * @param {number} statusCode + * @returns {Promise} + */ + async buildBroadcastTransactionResponse(sessionId, capabilities, statusCode) { + await this.#builder + .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE) + .setSessionId(sessionId) + .setTimestamp() + .setCapabilities(capabilities) + .setResultCode(statusCode) + .buildPayload(); + + return this.#builder.getResult(); + } + +} + +export default NetworkMessageDirector; diff --git a/src/messages/network/v1/networkMessageFactory.js b/src/messages/network/v1/networkMessageFactory.js new file mode 100644 index 00000000..c45f69b0 --- /dev/null +++ b/src/messages/network/v1/networkMessageFactory.js @@ -0,0 +1,12 @@ +import NetworkMessageDirector from "./NetworkMessageDirector.js"; +import NetworkMessageBuilder from "./NetworkMessageBuilder.js"; + +/** + * Factory helper to create a director with a fresh builder instance. + * @param {PeerWallet} wallet + * @param {object} config + * @returns {NetworkMessageDirector} + */ +export const networkMessageFactory = (wallet, config) => { + return new NetworkMessageDirector(new NetworkMessageBuilder(wallet, config)) +} diff --git a/src/messages/partialStateMessages/PartialStateMessageBuilder.js b/src/messages/partialStateMessages/PartialStateMessageBuilder.js deleted file mode 100644 index 1bf0191f..00000000 --- a/src/messages/partialStateMessages/PartialStateMessageBuilder.js +++ /dev/null @@ -1,272 +0,0 @@ -import PeerWallet from "trac-wallet"; -import b4a from "b4a"; - -import StateBuilder from '../base/StateBuilder.js' -import { OperationType } from '../../utils/constants.js'; -import { addressToBuffer, isAddressValid } from '../../core/state/utils/address.js'; -import { isHexString } from "../../utils/helpers.js"; -import { createMessage } from "../../utils/buffer.js"; -import { isTransaction, isRoleAccess, isBootstrapDeployment, isTransfer } from "../../utils/operations.js"; - -class PartialStateMessageBuilder extends StateBuilder { - #wallet; - #operationType; - #address; - #writingKey; - #txValidity; - #contentHash; - #externalBootstrap; - #withMsbBootstrap; - #channel; - #incomingAddress; - #amount; - #payload; - #config; - - /** - * @param {PeerWallet} wallet - * @param {object} config - **/ - constructor(wallet, config) { - super(); - this.#config = config; - if (!wallet || typeof wallet !== 'object') { - throw new Error('Wallet must be a valid wallet object'); - } - if (!isAddressValid(wallet.address, this.#config.addressPrefix)) { - throw new Error('Wallet should have a valid TRAC address.'); - } - - this.#wallet = wallet; - this.reset(); - } - - reset() { - this.#operationType = null; - this.#address = null; - this.#writingKey = null; - this.#txValidity = null; - this.#contentHash = null; - this.#externalBootstrap = null; - this.#withMsbBootstrap = false; - this.#incomingAddress = null; - this.#amount = null; - this.#channel = null; - this.#payload = {}; - } - - forOperationType(operationType) { - if (!Object.values(OperationType).includes(operationType) || OperationType === OperationType.UNKNOWN) { - throw new Error(`Invalid operation type: ${operationType}`); - } - - this.#operationType = operationType; - this.#payload.type = operationType; - return this; - } - - withAddress(address) { - if (!isAddressValid(address, this.#config.addressPrefix)) { - throw new Error(`Address field must be a valid TRAC bech32m address with length ${this.#config.addressLength}.`); - } - - this.#address = address; - this.#payload.address = this.#address; - return this; - } - - withContentHash(contentHash) { - if (!isHexString(contentHash) || contentHash.length !== 64) { - throw new Error('Content hash must be a 64-length hexstring.'); - } - this.#contentHash = contentHash; - return this; - } - - withExternalBootstrap(bootstrap) { - if (!isHexString(bootstrap) || bootstrap.length !== 64) { - throw new Error('Bootstrap key must be a 64-length hexstring.'); - } - this.#externalBootstrap = bootstrap; - return this; - } - - withMsbBootstrap(msbBootstrap) { - if (!isHexString(msbBootstrap) || msbBootstrap.length !== 64) { - throw new Error('MSB Bootstrap key must be a 64-length hexstring.'); - } - this.#withMsbBootstrap = msbBootstrap; - return this; - } - - withWriterKey(writerKey) { - if (!isHexString(writerKey) || writerKey.length !== 64) { - throw new Error('Writer key must be a 64-length hexstring.'); - } - this.#writingKey = writerKey; - return this; - } - - withTxValidity(txValidity) { - if (!isHexString(txValidity) || txValidity.length !== 64) { - throw new Error('txValidity must be a 64-length hexstring.'); - } - this.#txValidity = txValidity; - return this; - } - - withIncomingAddress(address) { - if (!isAddressValid(address, this.#config.addressPrefix)) { - throw new Error(`Incoming address field must be a valid TRAC bech32m address with length ${this.#config.addressLength}.`); - } - - this.#incomingAddress = address; - return this; - } - - withAmount(amount) { - if (!isHexString(amount) || amount.length !== 32) { - throw new Error('Amount must be a 32-length hexstring.'); - } - - this.#amount = amount; - return this; - } - - withChannel(channel) { - if (!isHexString(channel) || channel.length !== 64) { - throw new Error('Channel must be a 64-length hexstring.'); - } - - this.#channel = channel; - return this; - } - - async buildValueAndSign() { - const nonce = PeerWallet.generateNonce(); - let txMsg = null; - let tx = null; - let signature = null; - - // Creating a message for signing based on operation type - // ATTENTION REMEMBER THAT createMessage accept only BUFFER arguments and uint32 - switch (this.#operationType) { - case OperationType.ADD_WRITER: - case OperationType.REMOVE_WRITER: - case OperationType.ADMIN_RECOVERY: - txMsg = createMessage( - this.#config.networkId, - b4a.from(this.#txValidity, 'hex'), - b4a.from(this.#writingKey, 'hex'), - nonce, - this.#operationType - ); - break; - - case OperationType.BOOTSTRAP_DEPLOYMENT: - if (!this.#externalBootstrap) { - throw new Error('External bootstrap key must be set for BOOTSTRAP DEPLOYMENT operation.'); - } - txMsg = createMessage( - this.#config.networkId, - b4a.from(this.#txValidity, 'hex'), - b4a.from(this.#externalBootstrap, 'hex'), - b4a.from(this.#channel, 'hex'), - nonce, - OperationType.BOOTSTRAP_DEPLOYMENT - ); - break; - - case OperationType.TX: - txMsg = createMessage( - this.#config.networkId, - b4a.from(this.#txValidity, 'hex'), - b4a.from(this.#writingKey, 'hex'), - b4a.from(this.#contentHash, 'hex'), - b4a.from(this.#externalBootstrap, 'hex'), - b4a.from(this.#withMsbBootstrap, 'hex'), - nonce, - OperationType.TX - ); - break; - case OperationType.TRANSFER: - txMsg = createMessage( - this.#config.networkId, - b4a.from(this.#txValidity, 'hex'), - addressToBuffer(this.#incomingAddress, this.#config.addressPrefix), // we need to sign address of the recipient as well - b4a.from(this.#amount, 'hex'), - nonce, - OperationType.TRANSFER - ); - break; - default: - throw new Error(`Unsupported operation type: ${this.#operationType}`); - } - - // tx and signature - tx = await PeerWallet.blake3(txMsg); - signature = this.#wallet.sign(tx); - - // Build the payload based on operation type - if (isBootstrapDeployment(this.#operationType)) { - this.#payload.bdo = { - tx: tx.toString('hex'), - txv: this.#txValidity, - bs: this.#externalBootstrap, - ic: this.#channel, - in: nonce.toString('hex'), - is: signature.toString('hex') - }; - } else if (isRoleAccess(this.#operationType)) { - this.#payload.rao = { - tx: tx.toString('hex'), - txv: this.#txValidity, - iw: this.#writingKey, - in: nonce.toString('hex'), - is: signature.toString('hex') - }; - } else if (isTransaction(this.#operationType)) { - this.#payload.txo = { - tx: tx.toString('hex'), - txv: this.#txValidity, - iw: this.#writingKey, - ch: this.#contentHash, - bs: this.#externalBootstrap, - mbs: this.#withMsbBootstrap, - in: nonce.toString('hex'), - is: signature.toString('hex'), - }; - } else if (isTransfer(this.#operationType)) { - this.#payload.tro = { - tx: tx.toString('hex'), - txv: this.#txValidity, - to: this.#incomingAddress, - am: this.#amount, - in: nonce.toString('hex'), - is: signature.toString('hex') - } - } - - return this; - } - - getPayload() { - if ( - !this.#payload.type || - !this.#payload.address || - ( - !this.#payload.bdo && - !this.#payload.rao && - !this.#payload.txo && - !this.#payload.tro - ) - ) { - throw new Error('Product is not fully assembled. Missing type, address, or value bdo/rao/txo/tro.'); - } - const res = this.#payload; - this.reset(); - return res; - } -} - -export default PartialStateMessageBuilder; diff --git a/src/messages/partialStateMessages/PartialStateMessageDirector.js b/src/messages/partialStateMessages/PartialStateMessageDirector.js deleted file mode 100644 index a62dab6e..00000000 --- a/src/messages/partialStateMessages/PartialStateMessageDirector.js +++ /dev/null @@ -1,137 +0,0 @@ -import StateBuilder from '../base/StateBuilder.js' -import {OperationType} from '../../utils/constants.js' -import address from "../../core/state/utils/address.js"; - -class PartialStateMessageDirector { - #builder; - - set builder(builderInstance) { - if (!(builderInstance instanceof StateBuilder)) { - throw new Error('Director requires a Builder instance.'); - } - this.#builder = builderInstance; - } - - /** - * Builds a PARTIAL bootstrap deployment operation message, which can be sent to a validator. - * The validator can sign this operation to make it COMPLETE and broadcast it to the network. - * Bootstrap deployment is required to register a subnetwork. The network will reject - * TransactionOperation messages for external bootstraps that are not registered. - * Do NOT attempt to register the MSB bootstrap key. - * - * @param {String} address - Trac address of the requester/invoker node that broadcasts the operation. - * @param {String} bootstrap - Bootstrap key from the subnetwork to be registered. - * MUST be different from the MSB bootstrap key. - * BEFORE deploying, ensure the subnetwork bootstrap is not already deployed. - * @param {String} txValidity - Transaction validity hash representing the current indexer combination. - * The operation remains valid as long as indexer keys maintain their order. - * Acts as protection against deferred execution attacks. - * @returns {Promise} The built bootstrap deployment operation message. - * @throws {Error} If the builder has not been set or message building fails. - */ - async buildPartialBootstrapDeploymentMessage(address, bootstrap, channel, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.BOOTSTRAP_DEPLOYMENT) - .withAddress(address) - .withTxValidity(txValidity) - .withExternalBootstrap(bootstrap) - .withChannel(channel) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildAddWriterMessage(address, writingKey, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.ADD_WRITER) - .withAddress(address) - .withTxValidity(txValidity) - .withWriterKey(writingKey) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildRemoveWriterMessage(address, writerKey, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.REMOVE_WRITER) - .withAddress(address) - .withTxValidity(txValidity) - .withWriterKey(writerKey) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - async buildAdminRecoveryMessage(address, writingKey, txValidity) { - if (!this.#builder) throw new Error('Builder has not been set.'); - - await this.#builder - .forOperationType(OperationType.ADMIN_RECOVERY) - .withAddress(address) - .withTxValidity(txValidity) - .withWriterKey(writingKey) - .buildValueAndSign(); - - return this.#builder.getPayload(); - } - - /** - * Builds a transaction operation message for cross-network communication - * @param {String} address - Trac address of the requester/invoker node that broadcasts the transaction - * @param {String} incomingWritingKey - Writing key from the subnetwork, used for authentication of the requesting node - * @param {String} txValidity - Transaction validity hash representing current indexer combination. - * Transaction remains valid as long as indexer keys maintain their order. - * Acts as protection against deferred execution attacks. - * @param {String} contentHash - Hash of the contract content from the subnetwork, - * ensures data integrity between networks - * @param {String} externalBootstrap - Bootstrap key from the subnetwork, - * used for cross-network communication verification. - * MUST BE DIFFERENT from the MSB bootstrap key. - * transaction will be rejected if external bootstrap won't be - * deployed in the MSB (bootstrapDeploymentOperation). - * @param {String} msbBootstrap - Main Settlement Bus bootstrap key, - * used for internal network verification - * @returns {Promise} The built transaction operation message - * @throws {Error} If builder hasn't been set or if message building fails - */ - async buildTransactionOperationMessage( - address, - incomingWritingKey, - txValidity, - contentHash, - externalBootstrap, - msbBootstrap, - ) { - if (!this.#builder) throw new Error('Builder has not been set.'); - await this.#builder - .forOperationType(OperationType.TX) - .withAddress(address) - .withTxValidity(txValidity) - .withWriterKey(incomingWritingKey) - .withContentHash(contentHash) - .withExternalBootstrap(externalBootstrap) - .withMsbBootstrap(msbBootstrap) - .buildValueAndSign(); - return this.#builder.getPayload(); - } - async buildTransferOperationMessage(address, recipientAddress, amount, txValidity){ - if (!this.#builder) throw new Error('Builder has not been set.'); - await this.#builder - .forOperationType(OperationType.TRANSFER) - .withAddress(address) - .withTxValidity(txValidity) - .withIncomingAddress(recipientAddress) - .withAmount(amount) - .buildValueAndSign(); - return this.#builder.getPayload(); - } -} - -export default PartialStateMessageDirector; diff --git a/src/messages/partialStateMessages/PartialStateMessageOperations.js b/src/messages/partialStateMessages/PartialStateMessageOperations.js deleted file mode 100644 index 7fc6b0cd..00000000 --- a/src/messages/partialStateMessages/PartialStateMessageOperations.js +++ /dev/null @@ -1,138 +0,0 @@ -import PartialStateMessageBuilder from './PartialStateMessageBuilder.js'; -import PartialStateMessageDirector from './PartialStateMessageDirector.js'; - -class PartialStateMessageOperations { - #wallet; - #config - - /** - * @param {PeerWallet} wallet - Wallet of the requester/invoker node that broadcasts the operation - * @param {object} config - A configuration object - */ - constructor(wallet, config) { - this.#wallet = wallet; - this.#config = config; - } - - /** - * Assembles a PARTIAL bootstrap deployment operation, which can be sent to a validator. - * The validator can sign this operation to make it COMPLETE and broadcast it to the network. - * Bootstrap deployment is required to register a subnetwork. The network will reject - * TransactionOperation messages for external bootstraps that are not registered. - * Do NOT attempt to register the MSB bootstrap key. - * @param {String} externalBootstrap - Bootstrap key from the subnetwork to be registered. - * MUST be different from the MSB bootstrap key. - * BEFORE deploying ensure if the subnetwork bootstrap is not already deployed. - * @param {String} txValidity - Transaction validity hash representing the current indexer combination. - * The operation remains valid as long as indexer keys maintain their order. - * Acts as protection against deferred execution attacks. - * @returns {Promise} The assembled bootstrap deployment operation message - * @throws {Error} If assembly of the bootstrap deployment operation message fails - */ - async assembleBootstrapDeploymentMessage(externalBootstrap, channel, txValidity) { - try { - const builder = new PartialStateMessageBuilder(this.#wallet, this.#config); - const director = new PartialStateMessageDirector(); - director.builder = builder; - return await director.buildPartialBootstrapDeploymentMessage( - this.#wallet.address, - externalBootstrap, - channel, - txValidity - ); - } catch (error) { - throw new Error(`Failed to assemble partial bootstrap deployment message: ${error.message}`); - } - } - - async assembleAddWriterMessage(writingKey, txValidity) { - try { - const builder = new PartialStateMessageBuilder(this.#wallet, this.#config); - const director = new PartialStateMessageDirector(); - director.builder = builder; - return await director.buildAddWriterMessage(this.#wallet.address, writingKey, txValidity); - } catch (error) { - throw new Error(`Failed to assemble add writer message: ${error.message}`); - } - } - - async assembleRemoveWriterMessage(writerKey, txValidity) { - try { - const builder = new PartialStateMessageBuilder(this.#wallet, this.#config); - const director = new PartialStateMessageDirector(); - director.builder = builder; - return await director.buildRemoveWriterMessage(this.#wallet.address, writerKey, txValidity); - } catch (error) { - throw new Error(`Failed to assemble remove writer message: ${error.message}`); - } - } - - async assembleAdminRecoveryMessage(writingKey, txValidity) { - try { - const builder = new PartialStateMessageBuilder(this.#wallet, this.#config); - const director = new PartialStateMessageDirector(); - director.builder = builder; - return await director.buildAdminRecoveryMessage(this.#wallet.address, writingKey, txValidity); - } catch (error) { - throw new Error(`Failed to assemble admin recovery message: ${error.message}`); - } - } - - - /** - * Assembles a PARTIAL transaction operation, which can be sent to a validator, who can then - * sign the transaction to make it COMPLETE. - * @param {String} incomingWritingKey - Writing key from the subnetwork, used for authentication of the requesting node - * @param {String} txValidity - Transaction validity hash representing current indexer combination. - * Transaction remains valid as long as indexer keys maintain their order. - * Acts as protection against deferred execution attacks. - * @param {String} contentHash - Hash of the contract content from the subnetwork, - * ensures data integrity between networks - * @param {String} externalBootstrap - Bootstrap key from the subnetwork, - * used for cross-network communication verification. - * MUST BE DIFFERENT from the MSB bootstrap key. - * transaction will be rejected if external bootstrap won't be - * deployed in the MSB (bootstrapDeploymentOperation). - * @param {String} msbBootstrap - Main Settlement Bus bootstrap key, - * used for internal network verification - * @returns {Promise} The assembled transaction operation message - * @throws {Error} If assembly of the transaction operation message fails - */ - async assembleTransactionOperationMessage(incomingWritingKey, txValidity, contentHash, externalBootstrap, msbBootstrap) { - try { - const builder = new PartialStateMessageBuilder(this.#wallet, this.#config); - const director = new PartialStateMessageDirector(); - director.builder = builder; - return await director.buildTransactionOperationMessage( - this.#wallet.address, - incomingWritingKey, - txValidity, - contentHash, - externalBootstrap, - msbBootstrap - ); - } catch (error) { - throw new Error(`Failed to assemble transaction operation message: ${error.message}`); - } - } - - async assembleTransferOperationMessage(recipientAddress, amount, txValidity) { - try { - const builder = new PartialStateMessageBuilder(this.#wallet, this.#config); - const director = new PartialStateMessageDirector(); - director.builder = builder; - return await director.buildTransferOperationMessage( - this.#wallet.address, - recipientAddress, - amount, - txValidity - ); - } catch (error) { - throw new Error(`Failed to assemble transfer operation message: ${error.message}`); - } - - } - -} - -export default PartialStateMessageOperations; diff --git a/src/messages/state/ApplyStateMessageBuilder.js b/src/messages/state/ApplyStateMessageBuilder.js new file mode 100644 index 00000000..733fa26b --- /dev/null +++ b/src/messages/state/ApplyStateMessageBuilder.js @@ -0,0 +1,661 @@ +import b4a from 'b4a'; +import PeerWallet from 'trac-wallet'; + +import { createMessage } from '../../utils/buffer.js'; +import { OperationType } from '../../utils/constants.js'; +import { addressToBuffer, bufferToAddress } from '../../core/state/utils/address.js'; +import { isAddressValid } from "../../core/state/utils/address.js"; +import { + isAdminControl, + isBalanceInitialization, + isBootstrapDeployment, + isCoreAdmin, + isRoleAccess, + isTransaction, + isTransfer, + operationToPayload +} from '../../utils/applyOperations.js'; +import { isHexString } from '../../utils/helpers.js'; + +// Single use per transaction: reuse of this instance needs mutex/queue or fail-fast and can delay validation or break validation rule. +// A fresh instance is effectively zero-cost, so no reset() is provided. + +/** + * Builder for partial/complete ApplyState messages. + * @param {PeerWallet} wallet + * @param {object} config + */ +class ApplyStateMessageBuilder { + #address; + #amount; + #channel; + #config + #contentHash; + #externalBootstrap; + #incomingAddress; + #incomingNonce; + #incomingSignature; + #incomingWriterKey; + #msbBootstrap; + #operationType; + #payload; + #txHash; + #txValidity; + #wallet; + #writingKey; + #phase; + #output; + #payloadKey; + #built=false; + + constructor(wallet, config) { + this.#config = config; + if (!wallet || typeof wallet !== 'object') { + throw new Error('Wallet must be a valid wallet object'); + } + if (!isAddressValid(wallet.address, this.#config.addressPrefix)) { + throw new Error('Wallet should have a valid TRAC address.'); + } + + this.#wallet = wallet; + } + + setPhase(phase) { + if (!['partial', 'complete'].includes(phase)) { + throw new Error(`Invalid phase: ${phase}`); + } + this.#phase = phase; + return this; + } + + setOutput(output) { + if (!['json', 'buffer'].includes(output)) { + throw new Error(`Invalid output format: ${output}`); + } + this.#output = output; + return this; + } + + setOperationType(operationType) { + if (!Object.values(OperationType).includes(operationType)) { + throw new Error(`Invalid operation type: ${operationType}`); + } + this.#operationType = operationType; + return this; + } + + setAddress(address) { + const addressBuffer = this.#normalizeAddress(address); + if (!addressBuffer) { + throw new Error(`Address field must be a valid TRAC bech32m address with length ${this.#config.addressLength}.`); + } + + this.#address = addressBuffer; + return this; + } + + setWriterKey(writingKey) { + this.#writingKey = this.#normalizeHexBuffer(writingKey, 32, 'Writer key'); + return this; + } + + setTxHash(txHash) { + this.#txHash = this.#normalizeHexBuffer(txHash, 32, 'Transaction hash'); + return this; + } + + setIncomingAddress(address) { + const addressBuffer = this.#normalizeAddress(address); + if (!addressBuffer) { + throw new Error(`Address field must be a valid TRAC bech32m address with length ${this.#config.addressLength}.`); + } + + this.#incomingAddress = addressBuffer; + return this; + } + + setIncomingWriterKey(writerKey) { + this.#incomingWriterKey = this.#normalizeHexBuffer(writerKey, 32, 'Incoming writer key'); + return this; + } + + setIncomingNonce(nonce) { + this.#incomingNonce = this.#normalizeHexBuffer(nonce, 32, 'Incoming nonce'); + return this; + } + + setContentHash(contentHash) { + this.#contentHash = this.#normalizeHexBuffer(contentHash, 32, 'Content hash'); + return this; + } + + setIncomingSignature(signature) { + this.#incomingSignature = this.#normalizeHexBuffer(signature, 64, 'Incoming signature'); + return this; + } + + setExternalBootstrap(bootstrapKey) { + this.#externalBootstrap = this.#normalizeHexBuffer(bootstrapKey, 32, 'Bootstrap key'); + return this; + } + + setMsbBootstrap(msbBootstrap) { + this.#msbBootstrap = this.#normalizeHexBuffer(msbBootstrap, 32, 'MSB bootstrap'); + return this; + } + + setChannel(channel) { + this.#channel = this.#normalizeHexBuffer(channel, 32, 'Channel'); + return this; + } + + setTxValidity(txValidity) { + this.#txValidity = this.#normalizeHexBuffer(txValidity, 32, 'Transaction validity'); + return this; + } + + setAmount(amount) { + this.#amount = this.#normalizeHexBuffer(amount, 16, 'Amount'); + return this; + } + + #requireFields(fields) { + for (const [value, name] of fields) { + if (!value) { + throw new Error(`${name} must be set before build.`); + } + } + } + + async build() { + this.#assertPhaseAndOutput(); + + if (!this.#operationType) { + throw new Error('Operation type must be set before build.'); + } + + if (!this.#address) { + throw new Error('Address must be set before build.'); + } + + const payloadKey = operationToPayload(this.#operationType); + if (!payloadKey) { + throw new Error(`Unsupported operation type: ${this.#operationType}`); + } + + let body; + if (this.#phase === 'partial') { + body = await this.#buildPartialBody(); + } else { + body = await this.#buildCompleteBody(); + } + + this.#payloadKey = payloadKey; + this.#payload = { + type: this.#operationType, + address: this.#address, + [payloadKey]: body + }; + this.#built = true; + return this; + } + + getPayload() { + if (!this.#built || !this.#payload) { + throw new Error('Payload has not been built.'); + } + return this.#output === 'json' ? this.#encodePayloadJson(this.#payload) : this.#payload; + } + + #assertPhaseAndOutput() { + if (!this.#phase) { + throw new Error('Phase must be set before build.'); + } + + if (!this.#output) { + throw new Error('Output format must be set before build.'); + } + + // We assume that complete phase only supports buffer output. So this check will be enforced + if (this.#phase === 'complete' && this.#output !== 'buffer') { + throw new Error('Complete phase only supports buffer output.'); + } + } + + #normalizeHexBuffer(value, expectedBytes, fieldName) { + // with normalizer built in builder, we can remove other normalizers later. + if (b4a.isBuffer(value)) { + if (value.length !== expectedBytes) { + throw new Error(`${fieldName} must be a ${expectedBytes}-byte buffer.`); + } + return value; + } + if (typeof value === 'string') { + const expectedLength = expectedBytes * 2; + if (!isHexString(value) || value.length !== expectedLength) { + throw new Error(`${fieldName} must be a ${expectedLength}-length hexstring.`); + } + return b4a.from(value, 'hex'); + } + throw new Error(`${fieldName} must be a ${expectedBytes}-byte buffer or ${expectedBytes * 2}-length hexstring.`); + } + + #normalizeAddress(address) { + if (b4a.isBuffer(address)) { + const addr = bufferToAddress(address, this.#config.addressPrefix); + return addr ? address : null; + } + if (!isAddressValid(address, this.#config.addressPrefix)) { + return null; + } + return addressToBuffer(address, this.#config.addressPrefix); + } + + async #buildPartialBody() { + if (!isRoleAccess(this.#operationType) && !isTransaction(this.#operationType) && + !isBootstrapDeployment(this.#operationType) && !isTransfer(this.#operationType)) { + throw new Error(`Operation type ${this.#operationType} is not supported for partial build.`); + } + + const nonce = PeerWallet.generateNonce(); + let msg; + + switch (this.#operationType) { + case OperationType.ADD_WRITER: + case OperationType.REMOVE_WRITER: + case OperationType.ADMIN_RECOVERY: + this.#requireFields([ + [this.#txValidity, 'Transaction validity'], + [this.#writingKey, 'Writer key'] + ]); + msg = createMessage( + this.#config.networkId, + this.#txValidity, + this.#writingKey, + nonce, + this.#operationType + ); + break; + case OperationType.BOOTSTRAP_DEPLOYMENT: + this.#requireFields([ + [this.#txValidity, 'Transaction validity'], + [this.#externalBootstrap, 'External bootstrap'], + [this.#channel, 'Channel'] + ]); + msg = createMessage( + this.#config.networkId, + this.#txValidity, + this.#externalBootstrap, + this.#channel, + nonce, + OperationType.BOOTSTRAP_DEPLOYMENT + ); + break; + case OperationType.TX: + this.#requireFields([ + [this.#txValidity, 'Transaction validity'], + [this.#writingKey, 'Writer key'], + [this.#contentHash, 'Content hash'], + [this.#externalBootstrap, 'External bootstrap'], + [this.#msbBootstrap, 'MSB bootstrap'] + ]); + msg = createMessage( + this.#config.networkId, + this.#txValidity, + this.#writingKey, + this.#contentHash, + this.#externalBootstrap, + this.#msbBootstrap, + nonce, + OperationType.TX + ); + break; + case OperationType.TRANSFER: + this.#requireFields([ + [this.#txValidity, 'Transaction validity'], + [this.#incomingAddress, 'Incoming address'], + [this.#amount, 'Amount'] + ]); + msg = createMessage( + this.#config.networkId, + this.#txValidity, + this.#incomingAddress, + this.#amount, + nonce, + OperationType.TRANSFER + ); + break; + default: + throw new Error(`Unsupported operation type: ${this.#operationType}`); + } + + const tx = await PeerWallet.blake3(msg); + const signature = this.#wallet.sign(tx); + + if (isBootstrapDeployment(this.#operationType)) { + return { + tx, + txv: this.#txValidity, + bs: this.#externalBootstrap, + ic: this.#channel, + in: nonce, + is: signature + }; + } + if (isRoleAccess(this.#operationType)) { + return { + tx, + txv: this.#txValidity, + iw: this.#writingKey, + in: nonce, + is: signature + }; + } + if (isTransaction(this.#operationType)) { + return { + tx, + txv: this.#txValidity, + iw: this.#writingKey, + ch: this.#contentHash, + bs: this.#externalBootstrap, + mbs: this.#msbBootstrap, + in: nonce, + is: signature, + }; + } + if (isTransfer(this.#operationType)) { + return { + tx, + txv: this.#txValidity, + to: this.#incomingAddress, + am: this.#amount, + in: nonce, + is: signature + }; + } + + throw new Error(`No corresponding value type for operation: ${this.#operationType}`); + } + + async #buildCompleteBody() { + const nonce = PeerWallet.generateNonce(); + let msg; + + switch (this.#operationType) { + case OperationType.ADD_ADMIN: + case OperationType.DISABLE_INITIALIZATION: + this.#requireFields([ + [this.#txValidity, 'Transaction validity'], + [this.#writingKey, 'Writer key'] + ]); + msg = createMessage( + this.#config.networkId, + this.#txValidity, + this.#writingKey, + nonce, + this.#operationType + ); + break; + case OperationType.BALANCE_INITIALIZATION: + this.#requireFields([ + [this.#txValidity, 'Transaction validity'], + [this.#incomingAddress, 'Incoming address'], + [this.#amount, 'Amount'] + ]); + msg = createMessage( + this.#config.networkId, + this.#txValidity, + this.#incomingAddress, + this.#amount, + nonce, + this.#operationType + ); + break; + case OperationType.APPEND_WHITELIST: + case OperationType.ADD_INDEXER: + case OperationType.REMOVE_INDEXER: + case OperationType.BAN_VALIDATOR: { + this.#requireFields([ + [this.#txValidity, 'Transaction validity'], + [this.#incomingAddress, 'Incoming address'] + ]); + const incomingAddress = bufferToAddress(this.#incomingAddress, this.#config.addressPrefix); + if (incomingAddress && this.#wallet.address === incomingAddress) { + throw new Error('Address must not be the same as the wallet address for basic operations.'); + } + msg = createMessage( + this.#config.networkId, + this.#txValidity, + this.#incomingAddress, + nonce, + this.#operationType + ); + break; + } + case OperationType.ADD_WRITER: + case OperationType.REMOVE_WRITER: + case OperationType.ADMIN_RECOVERY: + this.#requireFields([ + [this.#txHash, 'Transaction hash'], + [this.#txValidity, 'Transaction validity'], + [this.#incomingWriterKey, 'Incoming writer key'], + [this.#incomingNonce, 'Incoming nonce'], + [this.#incomingSignature, 'Incoming signature'] + ]); + msg = createMessage( + this.#config.networkId, + this.#txHash, + nonce, + this.#operationType + ); + break; + case OperationType.BOOTSTRAP_DEPLOYMENT: + this.#requireFields([ + [this.#txHash, 'Transaction hash'], + [this.#txValidity, 'Transaction validity'], + [this.#externalBootstrap, 'External bootstrap'], + [this.#channel, 'Channel'], + [this.#incomingNonce, 'Incoming nonce'], + [this.#incomingSignature, 'Incoming signature'] + ]); + msg = createMessage( + this.#config.networkId, + this.#txHash, + nonce, + this.#operationType + ); + break; + case OperationType.TX: + this.#requireFields([ + [this.#txHash, 'Transaction hash'], + [this.#txValidity, 'Transaction validity'], + [this.#incomingWriterKey, 'Incoming writer key'], + [this.#incomingNonce, 'Incoming nonce'], + [this.#incomingSignature, 'Incoming signature'], + [this.#contentHash, 'Content hash'], + [this.#externalBootstrap, 'External bootstrap'], + [this.#msbBootstrap, 'MSB bootstrap'] + ]); + msg = createMessage( + this.#config.networkId, + this.#txHash, + nonce, + this.#operationType + ); + break; + case OperationType.TRANSFER: + this.#requireFields([ + [this.#txHash, 'Transaction hash'], + [this.#txValidity, 'Transaction validity'], + [this.#incomingAddress, 'Incoming address'], + [this.#amount, 'Amount'], + [this.#incomingNonce, 'Incoming nonce'], + [this.#incomingSignature, 'Incoming signature'] + ]); + msg = createMessage( + this.#config.networkId, + this.#txHash, + nonce, + this.#operationType + ); + break; + default: + throw new Error(`Unsupported operation type: ${this.#operationType}`); + } + + const tx = await PeerWallet.blake3(msg); + const signature = this.#wallet.sign(tx); + const validatorAddress = addressToBuffer(this.#wallet.address, this.#config.addressPrefix); + + if (isCoreAdmin(this.#operationType)) { + return { + tx, + txv: this.#txValidity, + iw: this.#writingKey, + in: nonce, + is: signature + }; + } + if (isAdminControl(this.#operationType)) { + return { + tx, + txv: this.#txValidity, + ia: this.#incomingAddress, + in: nonce, + is: signature + }; + } + if (isRoleAccess(this.#operationType)) { + return { + tx: this.#txHash, + txv: this.#txValidity, + iw: this.#incomingWriterKey, + in: this.#incomingNonce, + is: this.#incomingSignature, + va: validatorAddress, + vn: nonce, + vs: signature, + }; + } + if (isTransaction(this.#operationType)) { + return { + tx: this.#txHash, + txv: this.#txValidity, + iw: this.#incomingWriterKey, + ch: this.#contentHash, + bs: this.#externalBootstrap, + mbs: this.#msbBootstrap, + in: this.#incomingNonce, + is: this.#incomingSignature, + va: validatorAddress, + vn: nonce, + vs: signature, + }; + } + if (isBootstrapDeployment(this.#operationType)) { + return { + tx: this.#txHash, + txv: this.#txValidity, + bs: this.#externalBootstrap, + ic: this.#channel, + in: this.#incomingNonce, + is: this.#incomingSignature, + va: validatorAddress, + vn: nonce, + vs: signature + }; + } + if (isTransfer(this.#operationType)) { + return { + tx: this.#txHash, + txv: this.#txValidity, + to: this.#incomingAddress, + am: this.#amount, + in: this.#incomingNonce, + is: this.#incomingSignature, + va: validatorAddress, + vn: nonce, + vs: signature + }; + } + if (isBalanceInitialization(this.#operationType)) { + return { + tx, + txv: this.#txValidity, + ia: this.#incomingAddress, + am: this.#amount, + in: nonce, + is: signature + }; + } + + throw new Error(`No corresponding value type for operation: ${this.#operationType}`); + } + + #encodePayloadJson(payload) { + const toHex = buffer => buffer.toString('hex'); + const address = bufferToAddress(payload.address, this.#config.addressPrefix); + if (!address) { + throw new Error('Payload address is invalid.'); + } + + const body = payload[this.#payloadKey]; + const base = { type: payload.type, address }; + + switch (this.#payloadKey) { + case 'rao': + return { + ...base, + rao: { + tx: toHex(body.tx), + txv: toHex(body.txv), + iw: toHex(body.iw), + in: toHex(body.in), + is: toHex(body.is) + } + }; + case 'txo': + return { + ...base, + txo: { + tx: toHex(body.tx), + txv: toHex(body.txv), + iw: toHex(body.iw), + ch: toHex(body.ch), + bs: toHex(body.bs), + mbs: toHex(body.mbs), + in: toHex(body.in), + is: toHex(body.is) + } + }; + case 'bdo': + return { + ...base, + bdo: { + tx: toHex(body.tx), + txv: toHex(body.txv), + bs: toHex(body.bs), + ic: toHex(body.ic), + in: toHex(body.in), + is: toHex(body.is) + } + }; + case 'tro': + return { + ...base, + tro: { + tx: toHex(body.tx), + txv: toHex(body.txv), + to: bufferToAddress(body.to, this.#config.addressPrefix), + am: toHex(body.am), + in: toHex(body.in), + is: toHex(body.is) + } + }; + default: + throw new Error(`JSON output is not supported for payload ${this.#payloadKey}.`); + } + } +} + +export default ApplyStateMessageBuilder; diff --git a/src/messages/state/ApplyStateMessageDirector.js b/src/messages/state/ApplyStateMessageDirector.js new file mode 100644 index 00000000..b34c30a7 --- /dev/null +++ b/src/messages/state/ApplyStateMessageDirector.js @@ -0,0 +1,516 @@ +import { OperationType } from '../../utils/constants.js'; + +/** + * Director that orchestrates ApplyStateMessageBuilder for partial and complete messages. + */ +class ApplyStateMessageDirector { + #builder; + + /** + * @param {ApplyStateMessageBuilder} builderInstance + */ + constructor(builderInstance) { + this.#builder = builderInstance; + } + + /** + * Build a partial add writer payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} writingKey + * @param {string|Buffer} txValidity + * @param {'json'|'buffer'} output + * @returns {Promise} + */ + async buildPartialAddWriterMessage(invokerAddress, writingKey, txValidity, output) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('partial') + .setOutput(output) + .setOperationType(OperationType.ADD_WRITER) + .setAddress(invokerAddress) + .setTxValidity(txValidity) + .setWriterKey(writingKey) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a partial remove writer payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} writerKey + * @param {string|Buffer} txValidity + * @param {'json'|'buffer'} output + * @returns {Promise} + */ + async buildPartialRemoveWriterMessage(invokerAddress, writerKey, txValidity, output) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('partial') + .setOutput(output) + .setOperationType(OperationType.REMOVE_WRITER) + .setAddress(invokerAddress) + .setTxValidity(txValidity) + .setWriterKey(writerKey) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a partial admin recovery payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} writingKey + * @param {string|Buffer} txValidity + * @param {'json'|'buffer'} output + * @returns {Promise} + */ + async buildPartialAdminRecoveryMessage(invokerAddress, writingKey, txValidity, output) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('partial') + .setOutput(output) + .setOperationType(OperationType.ADMIN_RECOVERY) + .setAddress(invokerAddress) + .setTxValidity(txValidity) + .setWriterKey(writingKey) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a partial transaction payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} incomingWritingKey + * @param {string|Buffer} txValidity + * @param {string|Buffer} contentHash + * @param {string|Buffer} externalBootstrap + * @param {string|Buffer} msbBootstrap + * @param {'json'|'buffer'} output + * @returns {Promise} + */ + async buildPartialTransactionOperationMessage( + invokerAddress, + incomingWritingKey, + txValidity, + contentHash, + externalBootstrap, + msbBootstrap, + output + ) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('partial') + .setOutput(output) + .setOperationType(OperationType.TX) + .setAddress(invokerAddress) + .setTxValidity(txValidity) + .setWriterKey(incomingWritingKey) + .setContentHash(contentHash) + .setExternalBootstrap(externalBootstrap) + .setMsbBootstrap(msbBootstrap) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a partial bootstrap deployment payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} bootstrap + * @param {string|Buffer} channel + * @param {string|Buffer} txValidity + * @param {'json'|'buffer'} output + * @returns {Promise} + */ + async buildPartialBootstrapDeploymentMessage(invokerAddress, bootstrap, channel, txValidity, output) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('partial') + .setOutput(output) + .setOperationType(OperationType.BOOTSTRAP_DEPLOYMENT) + .setAddress(invokerAddress) + .setTxValidity(txValidity) + .setExternalBootstrap(bootstrap) + .setChannel(channel) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a partial transfer payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} recipientAddress + * @param {string|Buffer} amount + * @param {string|Buffer} txValidity + * @param {'json'|'buffer'} output + * @returns {Promise} + */ + async buildPartialTransferOperationMessage(invokerAddress, recipientAddress, amount, txValidity, output) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('partial') + .setOutput(output) + .setOperationType(OperationType.TRANSFER) + .setAddress(invokerAddress) + .setTxValidity(txValidity) + .setIncomingAddress(recipientAddress) + .setAmount(amount) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete add admin payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} writingKey + * @param {string|Buffer} txValidity + * @returns {Promise} + */ + async buildCompleteAddAdminMessage(invokerAddress, writingKey, txValidity) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.ADD_ADMIN) + .setAddress(invokerAddress) + .setWriterKey(writingKey) + .setTxValidity(txValidity) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete disable initialization payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} writingKey + * @param {string|Buffer} txValidity + * @returns {Promise} + */ + async buildCompleteDisableInitializationMessage(invokerAddress, writingKey, txValidity) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.DISABLE_INITIALIZATION) + .setAddress(invokerAddress) + .setWriterKey(writingKey) + .setTxValidity(txValidity) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete balance initialization payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} recipientAddress + * @param {string|Buffer} amount + * @param {string|Buffer} txValidity + * @returns {Promise} + */ + async buildCompleteBalanceInitializationMessage(invokerAddress, recipientAddress, amount, txValidity) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.BALANCE_INITIALIZATION) + .setAddress(invokerAddress) + .setIncomingAddress(recipientAddress) + .setAmount(amount) + .setTxValidity(txValidity) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete append whitelist payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} incomingAddress + * @param {string|Buffer} txValidity + * @returns {Promise} + */ + async buildCompleteAppendWhitelistMessage(invokerAddress, incomingAddress, txValidity) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.APPEND_WHITELIST) + .setAddress(invokerAddress) + .setTxValidity(txValidity) + .setIncomingAddress(incomingAddress) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete add writer payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} txHash + * @param {string|Buffer} txValidity + * @param {string|Buffer} incomingWritingKey + * @param {string|Buffer} incomingNonce + * @param {string|Buffer} incomingSignature + * @returns {Promise} + */ + async buildCompleteAddWriterMessage( + invokerAddress, + txHash, + txValidity, + incomingWritingKey, + incomingNonce, + incomingSignature + ) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.ADD_WRITER) + .setAddress(invokerAddress) + .setTxHash(txHash) + .setTxValidity(txValidity) + .setIncomingWriterKey(incomingWritingKey) + .setIncomingNonce(incomingNonce) + .setIncomingSignature(incomingSignature) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete remove writer payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} txHash + * @param {string|Buffer} txValidity + * @param {string|Buffer} incomingWritingKey + * @param {string|Buffer} incomingNonce + * @param {string|Buffer} incomingSignature + * @returns {Promise} + */ + async buildCompleteRemoveWriterMessage( + invokerAddress, + txHash, + txValidity, + incomingWritingKey, + incomingNonce, + incomingSignature + ) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.REMOVE_WRITER) + .setAddress(invokerAddress) + .setTxHash(txHash) + .setTxValidity(txValidity) + .setIncomingWriterKey(incomingWritingKey) + .setIncomingNonce(incomingNonce) + .setIncomingSignature(incomingSignature) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete admin recovery payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} txHash + * @param {string|Buffer} txValidity + * @param {string|Buffer} incomingWritingKey + * @param {string|Buffer} incomingNonce + * @param {string|Buffer} incomingSignature + * @returns {Promise} + */ + async buildCompleteAdminRecoveryMessage( + invokerAddress, + txHash, + txValidity, + incomingWritingKey, + incomingNonce, + incomingSignature + ) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.ADMIN_RECOVERY) + .setAddress(invokerAddress) + .setTxHash(txHash) + .setTxValidity(txValidity) + .setIncomingWriterKey(incomingWritingKey) + .setIncomingNonce(incomingNonce) + .setIncomingSignature(incomingSignature) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete add indexer payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} incomingAddress + * @param {string|Buffer} txValidity + * @returns {Promise} + */ + async buildCompleteAddIndexerMessage(invokerAddress, incomingAddress, txValidity) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.ADD_INDEXER) + .setAddress(invokerAddress) + .setTxValidity(txValidity) + .setIncomingAddress(incomingAddress) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete remove indexer payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} incomingAddress + * @param {string|Buffer} txValidity + * @returns {Promise} + */ + async buildCompleteRemoveIndexerMessage(invokerAddress, incomingAddress, txValidity) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.REMOVE_INDEXER) + .setAddress(invokerAddress) + .setTxValidity(txValidity) + .setIncomingAddress(incomingAddress) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete ban validator payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} incomingAddress + * @param {string|Buffer} txValidity + * @returns {Promise} + */ + async buildCompleteBanWriterMessage(invokerAddress, incomingAddress, txValidity) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.BAN_VALIDATOR) + .setAddress(invokerAddress) + .setTxValidity(txValidity) + .setIncomingAddress(incomingAddress) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete transaction payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} txHash + * @param {string|Buffer} txValidity + * @param {string|Buffer} incomingWriterKey + * @param {string|Buffer} incomingNonce + * @param {string|Buffer} contentHash + * @param {string|Buffer} incomingSignature + * @param {string|Buffer} externalBootstrap + * @param {string|Buffer} msbBootstrap + * @returns {Promise} + */ + async buildCompleteTransactionOperationMessage( + invokerAddress, + txHash, + txValidity, + incomingWriterKey, + incomingNonce, + contentHash, + incomingSignature, + externalBootstrap, + msbBootstrap + ) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.TX) + .setAddress(invokerAddress) + .setTxHash(txHash) + .setTxValidity(txValidity) + .setIncomingWriterKey(incomingWriterKey) + .setIncomingNonce(incomingNonce) + .setContentHash(contentHash) + .setIncomingSignature(incomingSignature) + .setExternalBootstrap(externalBootstrap) + .setMsbBootstrap(msbBootstrap) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete bootstrap deployment payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} transactionHash + * @param {string|Buffer} txValidity + * @param {string|Buffer} externalBootstrap + * @param {string|Buffer} channel + * @param {string|Buffer} incomingNonce + * @param {string|Buffer} incomingSignature + * @returns {Promise} + */ + async buildCompleteBootstrapDeploymentMessage( + invokerAddress, + transactionHash, + txValidity, + externalBootstrap, + channel, + incomingNonce, + incomingSignature + ) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.BOOTSTRAP_DEPLOYMENT) + .setAddress(invokerAddress) + .setTxHash(transactionHash) + .setTxValidity(txValidity) + .setExternalBootstrap(externalBootstrap) + .setChannel(channel) + .setIncomingNonce(incomingNonce) + .setIncomingSignature(incomingSignature) + .build(); + return this.#builder.getPayload(); + } + + /** + * Build a complete transfer payload. + * @param {string|Buffer} invokerAddress + * @param {string|Buffer} transactionHash + * @param {string|Buffer} txValidity + * @param {string|Buffer} incomingNonce + * @param {string|Buffer} recipientAddress + * @param {string|Buffer} amount + * @param {string|Buffer} incomingSignature + * @returns {Promise} + */ + async buildCompleteTransferOperationMessage( + invokerAddress, + transactionHash, + txValidity, + incomingNonce, + recipientAddress, + amount, + incomingSignature + ) { + if (!this.#builder) throw new Error('Builder has not been set.'); + await this.#builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.TRANSFER) + .setAddress(invokerAddress) + .setTxHash(transactionHash) + .setTxValidity(txValidity) + .setIncomingNonce(incomingNonce) + .setIncomingAddress(recipientAddress) + .setAmount(amount) + .setIncomingSignature(incomingSignature) + .build(); + return this.#builder.getPayload(); + } +} + +export default ApplyStateMessageDirector; diff --git a/src/messages/state/applyStateMessageFactory.js b/src/messages/state/applyStateMessageFactory.js new file mode 100644 index 00000000..ba7f30f8 --- /dev/null +++ b/src/messages/state/applyStateMessageFactory.js @@ -0,0 +1,12 @@ +import ApplyStateMessageDirector from "./ApplyStateMessageDirector.js"; +import ApplyStateMessageBuilder from "./ApplyStateMessageBuilder.js"; + +/** + * Factory helper to create a director with a builder instance. + * @param {PeerWallet} wallet + * @param {object} config + * @returns {ApplyStateMessageDirector} + */ +export const applyStateMessageFactory = (wallet, config) =>{ + return new ApplyStateMessageDirector(new ApplyStateMessageBuilder(wallet, config)) +} diff --git a/src/utils/operations.js b/src/utils/applyOperations.js similarity index 100% rename from src/utils/operations.js rename to src/utils/applyOperations.js diff --git a/src/utils/buffer.js b/src/utils/buffer.js index c0761a91..396ded0c 100644 --- a/src/utils/buffer.js +++ b/src/utils/buffer.js @@ -59,4 +59,50 @@ export function deepCopyBuffer(buffer) { const copy = b4a.alloc(buffer.length); buffer.copy(copy); return copy; -} \ No newline at end of file +} + +function uint64ToBuffer(value, fieldName) { + if (typeof value === 'number') { + if (!Number.isInteger(value) || value < 0) { + throw new Error(`${fieldName} must be a non-negative integer`); + } + value = BigInt(value); + } else if (typeof value !== 'bigint') { + throw new Error(`${fieldName} must be a number or bigint`); + } + + const buf = b4a.alloc(8); + buf.writeBigUInt64BE(value); + return buf; +} + +export function timestampToBuffer(timestamp) { + return uint64ToBuffer(timestamp, 'timestamp'); +} + +export function sessionIdToBuffer(sessionId) { + return uint64ToBuffer(sessionId, 'session id'); +} + + +export function encodeCapabilities(capabilities) { + if (!Array.isArray(capabilities)) { + throw new Error('Capabilities must be an array'); + } + const validCapabilities = capabilities.map((capability) => { + if (typeof capability !== 'string') { + throw new Error('Capabilities array must contain only strings'); + } + return capability; + }); + + const parts = []; + for (const capability of validCapabilities.slice().sort()) { + const capabilityBuffer = b4a.from(capability, 'utf8'); + const bufferLen = b4a.allocUnsafe(2); + bufferLen.writeUInt16BE(capabilityBuffer.length, 0); + parts.push(bufferLen, capabilityBuffer); + } + + return parts.length ? b4a.concat(parts) : b4a.alloc(0); +} diff --git a/src/utils/constants.js b/src/utils/constants.js index 5b5e26a6..1c40145b 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -1,4 +1,5 @@ -import { OperationType as OP } from './protobuf/applyOperations.cjs'; +import { OperationType as ApplyOperationType } from './protobuf/applyOperations.cjs'; +import { MessageType as NetworkMessageType, ResultCode as NetworkResultCode } from './protobuf/network.cjs'; import b4a from 'b4a' // TODO: We are going to have a lot of contstants. It would be good, to separate them into different files. @@ -17,19 +18,37 @@ export const EntryType = Object.freeze({ //ATTENTION - THIS IS USED IN THE APPLY FUNCTION! export const OperationType = Object.freeze({ - ADD_ADMIN: OP.ADD_ADMIN, - DISABLE_INITIALIZATION: OP.DISABLE_INITIALIZATION, - BALANCE_INITIALIZATION: OP.BALANCE_INITIALIZATION, - APPEND_WHITELIST: OP.APPEND_WHITELIST, - ADD_WRITER: OP.ADD_WRITER, - REMOVE_WRITER: OP.REMOVE_WRITER, - ADMIN_RECOVERY: OP.ADMIN_RECOVERY, - ADD_INDEXER: OP.ADD_INDEXER, - REMOVE_INDEXER: OP.REMOVE_INDEXER, - BAN_VALIDATOR: OP.BAN_VALIDATOR, - BOOTSTRAP_DEPLOYMENT: OP.BOOTSTRAP_DEPLOYMENT, - TX: OP.TX, - TRANSFER: OP.TRANSFER, + ADD_ADMIN: ApplyOperationType.ADD_ADMIN, + DISABLE_INITIALIZATION: ApplyOperationType.DISABLE_INITIALIZATION, + BALANCE_INITIALIZATION: ApplyOperationType.BALANCE_INITIALIZATION, + APPEND_WHITELIST: ApplyOperationType.APPEND_WHITELIST, + ADD_WRITER: ApplyOperationType.ADD_WRITER, + REMOVE_WRITER: ApplyOperationType.REMOVE_WRITER, + ADMIN_RECOVERY: ApplyOperationType.ADMIN_RECOVERY, + ADD_INDEXER: ApplyOperationType.ADD_INDEXER, + REMOVE_INDEXER: ApplyOperationType.REMOVE_INDEXER, + BAN_VALIDATOR: ApplyOperationType.BAN_VALIDATOR, + BOOTSTRAP_DEPLOYMENT: ApplyOperationType.BOOTSTRAP_DEPLOYMENT, + TX: ApplyOperationType.TX, + TRANSFER: ApplyOperationType.TRANSFER, +}); + +export const NetworkOperationType = Object.freeze({ + VALIDATOR_CONNECTION_REQUEST: NetworkMessageType.MESSAGE_TYPE_VALIDATOR_CONNECTION_REQUEST, + VALIDATOR_CONNECTION_RESPONSE: NetworkMessageType.MESSAGE_TYPE_VALIDATOR_CONNECTION_RESPONSE, + LIVENESS_REQUEST: NetworkMessageType.MESSAGE_TYPE_LIVENESS_REQUEST, + LIVENESS_RESPONSE: NetworkMessageType.MESSAGE_TYPE_LIVENESS_RESPONSE, + BROADCAST_TRANSACTION_REQUEST: NetworkMessageType.MESSAGE_TYPE_BROADCAST_TRANSACTION_REQUEST, + BROADCAST_TRANSACTION_RESPONSE: NetworkMessageType.MESSAGE_TYPE_BROADCAST_TRANSACTION_RESPONSE, +}); + +export const ResultCode = Object.freeze({ + OK: NetworkResultCode.RESULT_CODE_OK, + INVALID_PAYLOAD: NetworkResultCode.RESULT_CODE_INVALID_PAYLOAD, + UNSUPPORTED_VERSION: NetworkResultCode.RESULT_CODE_UNSUPPORTED_VERSION, + RATE_LIMITED: NetworkResultCode.RESULT_CODE_RATE_LIMITED, + TIMEOUT: NetworkResultCode.RESULT_CODE_TIMEOUT, + SIGNATURE_INVALID: NetworkResultCode.RESULT_CODE_SIGNATURE_INVALID, }); // Role managment constants @@ -69,6 +88,7 @@ export const MAX_PARALLEL = 64; export const MAX_SERVER_CONNECTIONS = Infinity; export const MAX_CLIENT_CONNECTIONS = Infinity; export const MAX_WRITERS_FOR_ADMIN_INDEXER_CONNECTION = 10; + // State export const ACK_INTERVAL = 1_000; export const AUTOBASE_VALUE_ENCODING = 'binary'; diff --git a/src/utils/normalizers.js b/src/utils/normalizers.js index 45ff6c52..590cda75 100644 --- a/src/utils/normalizers.js +++ b/src/utils/normalizers.js @@ -44,6 +44,14 @@ export function normalizeTransferOperation(payload, config) { }; } +/** + * Normalizes the payload for a transaction operation. + * This is useful for validating and assembling a transaction operation. + * + * @param {Object} payload The raw payload for the transaction operation. + * @param {object} config The environment configuration object. + * @returns {Object} A normalized payload with addresses converted to buffers and hex values normalized. + */ export function normalizeTransactionOperation(payload, config) { if (!payload || typeof payload !== 'object' || !payload.txo) { throw new Error('Invalid payload for transaction operation normalization.'); @@ -80,8 +88,9 @@ export function normalizeTransactionOperation(payload, config) { * Normalizes an operation payload by converting any Buffer values to hex strings. * This is useful for preparing a payload to be returned as a JSON response. * - * @param {Object} payload The decoded transaction payload. - * @returns {Object} A new object with Buffer values converted to hex strings. + * @param {Object} payload The raw payload for the role access operation. + * @param {object} config The environment configuration object. + * @returns {Object} A normalized payload with addresses converted to buffers and hex values normalized. */ export function normalizeDecodedPayloadForJson(payload, config) { if (!payload || typeof payload !== "object") { @@ -117,3 +126,76 @@ export function normalizeDecodedPayloadForJson(payload, config) { } return newPayload; } + +/** + * Normalizes the payload for a role access operation. + * This is useful for validating and assembling a role access operation. + * + * @param {Object} payload The raw payload for the role access operation. + * @param {object} config The environment configuration object. + * @returns {Object} A normalized payload with addresses converted to buffers and hex values normalized. + */ +export function normalizeRoleAccessOperation(payload, config) { + if (!payload || typeof payload !== 'object' || !payload.rao) { + throw new Error('Invalid payload for role access normalization.'); + } + const { type, address, rao } = payload; + if ( + !type || + !address || + !rao.tx || !rao.txv || !rao.iw || !rao.in || !rao.is + ) { + throw new Error('Missing required fields in role access payload.'); + } + + const normalizedRao = { + tx: normalizeHex(rao.tx), + txv: normalizeHex(rao.txv), + iw: normalizeHex(rao.iw), + in: normalizeHex(rao.in), + is: normalizeHex(rao.is) + }; + + return { + type, + address: addressToBuffer(address, config.addressPrefix), + rao: normalizedRao + }; +} + +/** + * Normalizes the payload for a bootstrap deployment operation. + * This is useful for validating and assembling a bootstrap deployment operation. + * + * @param {Object} payload The raw payload for the bootstrap deployment operation. + * @param {object} config The environment configuration object. + * @returns {Object} A normalized payload with addresses converted to buffers and hex values normalized. + */ +export function normalizeBootstrapDeploymentOperation(payload, config) { + if (!payload || typeof payload !== 'object' || !payload.bdo) { + throw new Error('Invalid payload for bootstrap deployment normalization.'); + } + const { type, address, bdo } = payload; + if ( + type !== OperationType.BOOTSTRAP_DEPLOYMENT || + !address || + !bdo.tx || !bdo.bs || !bdo.ic || !bdo.in || !bdo.is || !bdo.txv + ) { + throw new Error('Missing required fields in bootstrap deployment payload.'); + } + + const normalizedBdo = { + tx: normalizeHex(bdo.tx), // Transaction hash + txv: normalizeHex(bdo.txv), // Transaction validity + bs: normalizeHex(bdo.bs), // External bootstrap + ic: normalizeHex(bdo.ic), // Channel + in: normalizeHex(bdo.in), // Nonce + is: normalizeHex(bdo.is) // Signature + }; + + return { + type, + address: addressToBuffer(address, config.addressPrefix), + bdo: normalizedBdo + }; +} diff --git a/src/utils/protobuf/network.cjs b/src/utils/protobuf/network.cjs new file mode 100644 index 00000000..036b3649 --- /dev/null +++ b/src/utils/protobuf/network.cjs @@ -0,0 +1,840 @@ +var b4a = require('b4a'); +// This file is auto generated by the protocol-buffers compiler + +/* eslint-disable quotes */ +/* eslint-disable indent */ +/* eslint-disable no-redeclare */ +/* eslint-disable camelcase */ + +// Remember to `npm install --save protocol-buffers-encodings` +var encodings = require('protocol-buffers-encodings') +var varint = encodings.varint +var skip = encodings.skip + +exports.MessageType = { + "MESSAGE_TYPE_UNSPECIFIED": 0, + "MESSAGE_TYPE_VALIDATOR_CONNECTION_REQUEST": 1, + "MESSAGE_TYPE_VALIDATOR_CONNECTION_RESPONSE": 2, + "MESSAGE_TYPE_LIVENESS_REQUEST": 3, + "MESSAGE_TYPE_LIVENESS_RESPONSE": 4, + "MESSAGE_TYPE_BROADCAST_TRANSACTION_REQUEST": 5, + "MESSAGE_TYPE_BROADCAST_TRANSACTION_RESPONSE": 6 +} + +exports.ResultCode = { + "RESULT_CODE_UNSPECIFIED": 0, + "RESULT_CODE_OK": 1, + "RESULT_CODE_INVALID_PAYLOAD": 2, + "RESULT_CODE_UNSUPPORTED_VERSION": 3, + "RESULT_CODE_RATE_LIMITED": 4, + "RESULT_CODE_TIMEOUT": 5, + "RESULT_CODE_SIGNATURE_INVALID": 6 +} + +var ValidatorConnectionRequest = exports.ValidatorConnectionRequest = { + buffer: true, + encodingLength: null, + encode: null, + decode: null +} + +var ValidatorConnectionResponse = exports.ValidatorConnectionResponse = { + buffer: true, + encodingLength: null, + encode: null, + decode: null +} + +var LivenessRequest = exports.LivenessRequest = { + buffer: true, + encodingLength: null, + encode: null, + decode: null +} + +var LivenessResponse = exports.LivenessResponse = { + buffer: true, + encodingLength: null, + encode: null, + decode: null +} + +var BroadcastTransactionRequest = exports.BroadcastTransactionRequest = { + buffer: true, + encodingLength: null, + encode: null, + decode: null +} + +var BroadcastTransactionResponse = exports.BroadcastTransactionResponse = { + buffer: true, + encodingLength: null, + encode: null, + decode: null +} + +var MessageHeader = exports.MessageHeader = { + buffer: true, + encodingLength: null, + encode: null, + decode: null +} + +defineValidatorConnectionRequest() +defineValidatorConnectionResponse() +defineLivenessRequest() +defineLivenessResponse() +defineBroadcastTransactionRequest() +defineBroadcastTransactionResponse() +defineMessageHeader() + +function defineValidatorConnectionRequest () { + ValidatorConnectionRequest.encodingLength = encodingLength + ValidatorConnectionRequest.encode = encode + ValidatorConnectionRequest.decode = decode + + function encodingLength (obj) { + var length = 0 + if (defined(obj.issuer_address)) { + var len = encodings.string.encodingLength(obj.issuer_address) + length += 1 + len + } + if (defined(obj.nonce)) { + var len = encodings.bytes.encodingLength(obj.nonce) + length += 1 + len + } + if (defined(obj.signature)) { + var len = encodings.bytes.encodingLength(obj.signature) + length += 1 + len + } + return length + } + + function encode (obj, buf, offset) { + if (!offset) offset = 0 + if (!buf) buf = b4a.allocUnsafe(encodingLength(obj)) + var oldOffset = offset + if (defined(obj.issuer_address)) { + buf[offset++] = 10 + encodings.string.encode(obj.issuer_address, buf, offset) + offset += encodings.string.encode.bytes + } + if (defined(obj.nonce)) { + buf[offset++] = 18 + encodings.bytes.encode(obj.nonce, buf, offset) + offset += encodings.bytes.encode.bytes + } + if (defined(obj.signature)) { + buf[offset++] = 26 + encodings.bytes.encode(obj.signature, buf, offset) + offset += encodings.bytes.encode.bytes + } + encode.bytes = offset - oldOffset + return buf + } + + function decode (buf, offset, end) { + if (!offset) offset = 0 + if (!end) end = buf.length + if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") + var oldOffset = offset + var obj = { + issuer_address: "", + nonce: null, + signature: null + } + while (true) { + if (end <= offset) { + decode.bytes = offset - oldOffset + return obj + } + var prefix = varint.decode(buf, offset) + offset += varint.decode.bytes + var tag = prefix >> 3 + switch (tag) { + case 1: + obj.issuer_address = encodings.string.decode(buf, offset) + offset += encodings.string.decode.bytes + break + case 2: + obj.nonce = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + case 3: + obj.signature = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + default: + offset = skip(prefix & 7, buf, offset) + } + } + } +} + +function defineValidatorConnectionResponse () { + ValidatorConnectionResponse.encodingLength = encodingLength + ValidatorConnectionResponse.encode = encode + ValidatorConnectionResponse.decode = decode + + function encodingLength (obj) { + var length = 0 + if (defined(obj.issuer_address)) { + var len = encodings.string.encodingLength(obj.issuer_address) + length += 1 + len + } + if (defined(obj.nonce)) { + var len = encodings.bytes.encodingLength(obj.nonce) + length += 1 + len + } + if (defined(obj.signature)) { + var len = encodings.bytes.encodingLength(obj.signature) + length += 1 + len + } + if (defined(obj.result)) { + var len = encodings.enum.encodingLength(obj.result) + length += 1 + len + } + return length + } + + function encode (obj, buf, offset) { + if (!offset) offset = 0 + if (!buf) buf = b4a.allocUnsafe(encodingLength(obj)) + var oldOffset = offset + if (defined(obj.issuer_address)) { + buf[offset++] = 10 + encodings.string.encode(obj.issuer_address, buf, offset) + offset += encodings.string.encode.bytes + } + if (defined(obj.nonce)) { + buf[offset++] = 18 + encodings.bytes.encode(obj.nonce, buf, offset) + offset += encodings.bytes.encode.bytes + } + if (defined(obj.signature)) { + buf[offset++] = 26 + encodings.bytes.encode(obj.signature, buf, offset) + offset += encodings.bytes.encode.bytes + } + if (defined(obj.result)) { + buf[offset++] = 32 + encodings.enum.encode(obj.result, buf, offset) + offset += encodings.enum.encode.bytes + } + encode.bytes = offset - oldOffset + return buf + } + + function decode (buf, offset, end) { + if (!offset) offset = 0 + if (!end) end = buf.length + if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") + var oldOffset = offset + var obj = { + issuer_address: "", + nonce: null, + signature: null, + result: 0 + } + while (true) { + if (end <= offset) { + decode.bytes = offset - oldOffset + return obj + } + var prefix = varint.decode(buf, offset) + offset += varint.decode.bytes + var tag = prefix >> 3 + switch (tag) { + case 1: + obj.issuer_address = encodings.string.decode(buf, offset) + offset += encodings.string.decode.bytes + break + case 2: + obj.nonce = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + case 3: + obj.signature = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + case 4: + obj.result = encodings.enum.decode(buf, offset) + offset += encodings.enum.decode.bytes + break + default: + offset = skip(prefix & 7, buf, offset) + } + } + } +} + +function defineLivenessRequest () { + LivenessRequest.encodingLength = encodingLength + LivenessRequest.encode = encode + LivenessRequest.decode = decode + + function encodingLength (obj) { + var length = 0 + if (defined(obj.nonce)) { + var len = encodings.bytes.encodingLength(obj.nonce) + length += 1 + len + } + if (defined(obj.signature)) { + var len = encodings.bytes.encodingLength(obj.signature) + length += 1 + len + } + return length + } + + function encode (obj, buf, offset) { + if (!offset) offset = 0 + if (!buf) buf = b4a.allocUnsafe(encodingLength(obj)) + var oldOffset = offset + if (defined(obj.nonce)) { + buf[offset++] = 10 + encodings.bytes.encode(obj.nonce, buf, offset) + offset += encodings.bytes.encode.bytes + } + if (defined(obj.signature)) { + buf[offset++] = 18 + encodings.bytes.encode(obj.signature, buf, offset) + offset += encodings.bytes.encode.bytes + } + encode.bytes = offset - oldOffset + return buf + } + + function decode (buf, offset, end) { + if (!offset) offset = 0 + if (!end) end = buf.length + if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") + var oldOffset = offset + var obj = { + nonce: null, + signature: null + } + while (true) { + if (end <= offset) { + decode.bytes = offset - oldOffset + return obj + } + var prefix = varint.decode(buf, offset) + offset += varint.decode.bytes + var tag = prefix >> 3 + switch (tag) { + case 1: + obj.nonce = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + case 2: + obj.signature = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + default: + offset = skip(prefix & 7, buf, offset) + } + } + } +} + +function defineLivenessResponse () { + LivenessResponse.encodingLength = encodingLength + LivenessResponse.encode = encode + LivenessResponse.decode = decode + + function encodingLength (obj) { + var length = 0 + if (defined(obj.nonce)) { + var len = encodings.bytes.encodingLength(obj.nonce) + length += 1 + len + } + if (defined(obj.signature)) { + var len = encodings.bytes.encodingLength(obj.signature) + length += 1 + len + } + if (defined(obj.result)) { + var len = encodings.enum.encodingLength(obj.result) + length += 1 + len + } + return length + } + + function encode (obj, buf, offset) { + if (!offset) offset = 0 + if (!buf) buf = b4a.allocUnsafe(encodingLength(obj)) + var oldOffset = offset + if (defined(obj.nonce)) { + buf[offset++] = 10 + encodings.bytes.encode(obj.nonce, buf, offset) + offset += encodings.bytes.encode.bytes + } + if (defined(obj.signature)) { + buf[offset++] = 18 + encodings.bytes.encode(obj.signature, buf, offset) + offset += encodings.bytes.encode.bytes + } + if (defined(obj.result)) { + buf[offset++] = 24 + encodings.enum.encode(obj.result, buf, offset) + offset += encodings.enum.encode.bytes + } + encode.bytes = offset - oldOffset + return buf + } + + function decode (buf, offset, end) { + if (!offset) offset = 0 + if (!end) end = buf.length + if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") + var oldOffset = offset + var obj = { + nonce: null, + signature: null, + result: 0 + } + while (true) { + if (end <= offset) { + decode.bytes = offset - oldOffset + return obj + } + var prefix = varint.decode(buf, offset) + offset += varint.decode.bytes + var tag = prefix >> 3 + switch (tag) { + case 1: + obj.nonce = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + case 2: + obj.signature = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + case 3: + obj.result = encodings.enum.decode(buf, offset) + offset += encodings.enum.decode.bytes + break + default: + offset = skip(prefix & 7, buf, offset) + } + } + } +} + +function defineBroadcastTransactionRequest () { + BroadcastTransactionRequest.encodingLength = encodingLength + BroadcastTransactionRequest.encode = encode + BroadcastTransactionRequest.decode = decode + + function encodingLength (obj) { + var length = 0 + if (defined(obj.data)) { + var len = encodings.bytes.encodingLength(obj.data) + length += 1 + len + } + if (defined(obj.nonce)) { + var len = encodings.bytes.encodingLength(obj.nonce) + length += 1 + len + } + if (defined(obj.signature)) { + var len = encodings.bytes.encodingLength(obj.signature) + length += 1 + len + } + return length + } + + function encode (obj, buf, offset) { + if (!offset) offset = 0 + if (!buf) buf = b4a.allocUnsafe(encodingLength(obj)) + var oldOffset = offset + if (defined(obj.data)) { + buf[offset++] = 10 + encodings.bytes.encode(obj.data, buf, offset) + offset += encodings.bytes.encode.bytes + } + if (defined(obj.nonce)) { + buf[offset++] = 18 + encodings.bytes.encode(obj.nonce, buf, offset) + offset += encodings.bytes.encode.bytes + } + if (defined(obj.signature)) { + buf[offset++] = 26 + encodings.bytes.encode(obj.signature, buf, offset) + offset += encodings.bytes.encode.bytes + } + encode.bytes = offset - oldOffset + return buf + } + + function decode (buf, offset, end) { + if (!offset) offset = 0 + if (!end) end = buf.length + if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") + var oldOffset = offset + var obj = { + data: null, + nonce: null, + signature: null + } + while (true) { + if (end <= offset) { + decode.bytes = offset - oldOffset + return obj + } + var prefix = varint.decode(buf, offset) + offset += varint.decode.bytes + var tag = prefix >> 3 + switch (tag) { + case 1: + obj.data = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + case 2: + obj.nonce = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + case 3: + obj.signature = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + default: + offset = skip(prefix & 7, buf, offset) + } + } + } +} + +function defineBroadcastTransactionResponse () { + BroadcastTransactionResponse.encodingLength = encodingLength + BroadcastTransactionResponse.encode = encode + BroadcastTransactionResponse.decode = decode + + function encodingLength (obj) { + var length = 0 + if (defined(obj.nonce)) { + var len = encodings.bytes.encodingLength(obj.nonce) + length += 1 + len + } + if (defined(obj.signature)) { + var len = encodings.bytes.encodingLength(obj.signature) + length += 1 + len + } + if (defined(obj.result)) { + var len = encodings.enum.encodingLength(obj.result) + length += 1 + len + } + return length + } + + function encode (obj, buf, offset) { + if (!offset) offset = 0 + if (!buf) buf = b4a.allocUnsafe(encodingLength(obj)) + var oldOffset = offset + if (defined(obj.nonce)) { + buf[offset++] = 10 + encodings.bytes.encode(obj.nonce, buf, offset) + offset += encodings.bytes.encode.bytes + } + if (defined(obj.signature)) { + buf[offset++] = 18 + encodings.bytes.encode(obj.signature, buf, offset) + offset += encodings.bytes.encode.bytes + } + if (defined(obj.result)) { + buf[offset++] = 24 + encodings.enum.encode(obj.result, buf, offset) + offset += encodings.enum.encode.bytes + } + encode.bytes = offset - oldOffset + return buf + } + + function decode (buf, offset, end) { + if (!offset) offset = 0 + if (!end) end = buf.length + if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") + var oldOffset = offset + var obj = { + nonce: null, + signature: null, + result: 0 + } + while (true) { + if (end <= offset) { + decode.bytes = offset - oldOffset + return obj + } + var prefix = varint.decode(buf, offset) + offset += varint.decode.bytes + var tag = prefix >> 3 + switch (tag) { + case 1: + obj.nonce = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + case 2: + obj.signature = encodings.bytes.decode(buf, offset) + offset += encodings.bytes.decode.bytes + break + case 3: + obj.result = encodings.enum.decode(buf, offset) + offset += encodings.enum.decode.bytes + break + default: + offset = skip(prefix & 7, buf, offset) + } + } + } +} + +function defineMessageHeader () { + MessageHeader.encodingLength = encodingLength + MessageHeader.encode = encode + MessageHeader.decode = decode + + function encodingLength (obj) { + var length = 0 + if ((+defined(obj.validator_connection_request) + +defined(obj.validator_connection_response) + +defined(obj.liveness_request) + +defined(obj.liveness_response) + +defined(obj.broadcast_transaction_request) + +defined(obj.broadcast_transaction_response)) > 1) throw new Error("only one of the properties defined in oneof field can be set") + if (defined(obj.type)) { + var len = encodings.enum.encodingLength(obj.type) + length += 1 + len + } + if (defined(obj.session_id)) { + var len = encodings.varint.encodingLength(obj.session_id) + length += 1 + len + } + if (defined(obj.timestamp)) { + var len = encodings.varint.encodingLength(obj.timestamp) + length += 1 + len + } + if (defined(obj.validator_connection_request)) { + var len = ValidatorConnectionRequest.encodingLength(obj.validator_connection_request) + length += varint.encodingLength(len) + length += 1 + len + } + if (defined(obj.validator_connection_response)) { + var len = ValidatorConnectionResponse.encodingLength(obj.validator_connection_response) + length += varint.encodingLength(len) + length += 1 + len + } + if (defined(obj.liveness_request)) { + var len = LivenessRequest.encodingLength(obj.liveness_request) + length += varint.encodingLength(len) + length += 1 + len + } + if (defined(obj.liveness_response)) { + var len = LivenessResponse.encodingLength(obj.liveness_response) + length += varint.encodingLength(len) + length += 1 + len + } + if (defined(obj.broadcast_transaction_request)) { + var len = BroadcastTransactionRequest.encodingLength(obj.broadcast_transaction_request) + length += varint.encodingLength(len) + length += 1 + len + } + if (defined(obj.broadcast_transaction_response)) { + var len = BroadcastTransactionResponse.encodingLength(obj.broadcast_transaction_response) + length += varint.encodingLength(len) + length += 1 + len + } + if (defined(obj.capabilities)) { + for (var i = 0; i < obj.capabilities.length; i++) { + if (!defined(obj.capabilities[i])) continue + var len = encodings.string.encodingLength(obj.capabilities[i]) + length += 1 + len + } + } + return length + } + + function encode (obj, buf, offset) { + if (!offset) offset = 0 + if (!buf) buf = b4a.allocUnsafe(encodingLength(obj)) + var oldOffset = offset + if ((+defined(obj.validator_connection_request) + +defined(obj.validator_connection_response) + +defined(obj.liveness_request) + +defined(obj.liveness_response) + +defined(obj.broadcast_transaction_request) + +defined(obj.broadcast_transaction_response)) > 1) throw new Error("only one of the properties defined in oneof field can be set") + if (defined(obj.type)) { + buf[offset++] = 8 + encodings.enum.encode(obj.type, buf, offset) + offset += encodings.enum.encode.bytes + } + if (defined(obj.session_id)) { + buf[offset++] = 16 + encodings.varint.encode(obj.session_id, buf, offset) + offset += encodings.varint.encode.bytes + } + if (defined(obj.timestamp)) { + buf[offset++] = 24 + encodings.varint.encode(obj.timestamp, buf, offset) + offset += encodings.varint.encode.bytes + } + if (defined(obj.validator_connection_request)) { + buf[offset++] = 34 + varint.encode(ValidatorConnectionRequest.encodingLength(obj.validator_connection_request), buf, offset) + offset += varint.encode.bytes + ValidatorConnectionRequest.encode(obj.validator_connection_request, buf, offset) + offset += ValidatorConnectionRequest.encode.bytes + } + if (defined(obj.validator_connection_response)) { + buf[offset++] = 42 + varint.encode(ValidatorConnectionResponse.encodingLength(obj.validator_connection_response), buf, offset) + offset += varint.encode.bytes + ValidatorConnectionResponse.encode(obj.validator_connection_response, buf, offset) + offset += ValidatorConnectionResponse.encode.bytes + } + if (defined(obj.liveness_request)) { + buf[offset++] = 50 + varint.encode(LivenessRequest.encodingLength(obj.liveness_request), buf, offset) + offset += varint.encode.bytes + LivenessRequest.encode(obj.liveness_request, buf, offset) + offset += LivenessRequest.encode.bytes + } + if (defined(obj.liveness_response)) { + buf[offset++] = 58 + varint.encode(LivenessResponse.encodingLength(obj.liveness_response), buf, offset) + offset += varint.encode.bytes + LivenessResponse.encode(obj.liveness_response, buf, offset) + offset += LivenessResponse.encode.bytes + } + if (defined(obj.broadcast_transaction_request)) { + buf[offset++] = 66 + varint.encode(BroadcastTransactionRequest.encodingLength(obj.broadcast_transaction_request), buf, offset) + offset += varint.encode.bytes + BroadcastTransactionRequest.encode(obj.broadcast_transaction_request, buf, offset) + offset += BroadcastTransactionRequest.encode.bytes + } + if (defined(obj.broadcast_transaction_response)) { + buf[offset++] = 74 + varint.encode(BroadcastTransactionResponse.encodingLength(obj.broadcast_transaction_response), buf, offset) + offset += varint.encode.bytes + BroadcastTransactionResponse.encode(obj.broadcast_transaction_response, buf, offset) + offset += BroadcastTransactionResponse.encode.bytes + } + if (defined(obj.capabilities)) { + for (var i = 0; i < obj.capabilities.length; i++) { + if (!defined(obj.capabilities[i])) continue + buf[offset++] = 82 + encodings.string.encode(obj.capabilities[i], buf, offset) + offset += encodings.string.encode.bytes + } + } + encode.bytes = offset - oldOffset + return buf + } + + function decode (buf, offset, end) { + if (!offset) offset = 0 + if (!end) end = buf.length + if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") + var oldOffset = offset + var obj = { + type: 0, + session_id: 0, + timestamp: 0, + validator_connection_request: null, + validator_connection_response: null, + liveness_request: null, + liveness_response: null, + broadcast_transaction_request: null, + broadcast_transaction_response: null, + capabilities: [] + } + while (true) { + if (end <= offset) { + decode.bytes = offset - oldOffset + return obj + } + var prefix = varint.decode(buf, offset) + offset += varint.decode.bytes + var tag = prefix >> 3 + switch (tag) { + case 1: + obj.type = encodings.enum.decode(buf, offset) + offset += encodings.enum.decode.bytes + break + case 2: + obj.session_id = encodings.varint.decode(buf, offset) + offset += encodings.varint.decode.bytes + break + case 3: + obj.timestamp = encodings.varint.decode(buf, offset) + offset += encodings.varint.decode.bytes + break + case 4: + delete obj.validator_connection_response + delete obj.liveness_request + delete obj.liveness_response + delete obj.broadcast_transaction_request + delete obj.broadcast_transaction_response + var len = varint.decode(buf, offset) + offset += varint.decode.bytes + obj.validator_connection_request = ValidatorConnectionRequest.decode(buf, offset, offset + len) + offset += ValidatorConnectionRequest.decode.bytes + break + case 5: + delete obj.validator_connection_request + delete obj.liveness_request + delete obj.liveness_response + delete obj.broadcast_transaction_request + delete obj.broadcast_transaction_response + var len = varint.decode(buf, offset) + offset += varint.decode.bytes + obj.validator_connection_response = ValidatorConnectionResponse.decode(buf, offset, offset + len) + offset += ValidatorConnectionResponse.decode.bytes + break + case 6: + delete obj.validator_connection_request + delete obj.validator_connection_response + delete obj.liveness_response + delete obj.broadcast_transaction_request + delete obj.broadcast_transaction_response + var len = varint.decode(buf, offset) + offset += varint.decode.bytes + obj.liveness_request = LivenessRequest.decode(buf, offset, offset + len) + offset += LivenessRequest.decode.bytes + break + case 7: + delete obj.validator_connection_request + delete obj.validator_connection_response + delete obj.liveness_request + delete obj.broadcast_transaction_request + delete obj.broadcast_transaction_response + var len = varint.decode(buf, offset) + offset += varint.decode.bytes + obj.liveness_response = LivenessResponse.decode(buf, offset, offset + len) + offset += LivenessResponse.decode.bytes + break + case 8: + delete obj.validator_connection_request + delete obj.validator_connection_response + delete obj.liveness_request + delete obj.liveness_response + delete obj.broadcast_transaction_response + var len = varint.decode(buf, offset) + offset += varint.decode.bytes + obj.broadcast_transaction_request = BroadcastTransactionRequest.decode(buf, offset, offset + len) + offset += BroadcastTransactionRequest.decode.bytes + break + case 9: + delete obj.validator_connection_request + delete obj.validator_connection_response + delete obj.liveness_request + delete obj.liveness_response + delete obj.broadcast_transaction_request + var len = varint.decode(buf, offset) + offset += varint.decode.bytes + obj.broadcast_transaction_response = BroadcastTransactionResponse.decode(buf, offset, offset + len) + offset += BroadcastTransactionResponse.decode.bytes + break + case 10: + obj.capabilities.push(encodings.string.decode(buf, offset)) + offset += encodings.string.decode.bytes + break + default: + offset = skip(prefix & 7, buf, offset) + } + } + } +} + +function defined (val) { + return val !== null && val !== undefined && (typeof val !== 'number' || !isNaN(val)) +} diff --git a/src/utils/protobuf/operationHelpers.js b/src/utils/protobuf/operationHelpers.js index cae95ccc..795fd9f5 100644 --- a/src/utils/protobuf/operationHelpers.js +++ b/src/utils/protobuf/operationHelpers.js @@ -1,4 +1,5 @@ import applyOperations from './applyOperations.cjs'; +import networkV1Operations from './network.cjs'; import b4a from 'b4a'; /** @@ -48,3 +49,12 @@ export const normalizeIncomingMessage = (message) => { return null; }; + +export const encodeV1networkOperation = (payload) => { + return networkV1Operations.MessageHeader.encode(payload); +} + + +export const decodeV1networkOperation = (payload) => { + return networkV1Operations.MessageHeader.decode(payload); +} diff --git a/tests/acceptance/v1/rpc.test.mjs b/tests/acceptance/v1/rpc.test.mjs index 33302b22..f56737a6 100644 --- a/tests/acceptance/v1/rpc.test.mjs +++ b/tests/acceptance/v1/rpc.test.mjs @@ -58,7 +58,7 @@ const setupNetwork = async () => { beforeAll(async () => { const { admin, writer, reader } = await setupNetwork() - const server = createServer(reader.msb) + const server = createServer(reader.msb, reader.config) toClose = admin.msb Object.assign(testContext, { writerMsb: writer.msb, diff --git a/tests/fixtures/networkV1.fixtures.js b/tests/fixtures/networkV1.fixtures.js new file mode 100644 index 00000000..95b97eee --- /dev/null +++ b/tests/fixtures/networkV1.fixtures.js @@ -0,0 +1,84 @@ +import b4a from 'b4a'; +import { NetworkOperationType, ResultCode as NetworkResultCode } from '../../src/utils/constants.js'; + +const payloadValidatorConnectionRequest = { + type: NetworkOperationType.VALIDATOR_CONNECTION_REQUEST, + session_id: 1, + timestamp: 123, + validator_connection_request: { + issuer_address: 'trac1xm76l9qaujh7vqktk8302mw9sfrxau3l45w62hqfl4kasswt6yts0autkh', + nonce: b4a.from('00', 'hex'), + signature: b4a.from('01', 'hex') + }, + capabilities: ['cap:a', 'cap:b'] +}; + +const payloadValidatorConnectionResponse = { + type: NetworkOperationType.VALIDATOR_CONNECTION_RESPONSE, + session_id: 2, + timestamp: 456, + validator_connection_response: { + issuer_address: 'trac1xm76l9qaujh7vqktk8302mw9sfrxau3l45w62hqfl4kasswt6yts0autkh', + nonce: b4a.from('02', 'hex'), + signature: b4a.from('03', 'hex'), + result: NetworkResultCode.OK + }, + capabilities: ['cap:a'] +}; + +const payloadLivenessRequest = { + type: NetworkOperationType.LIVENESS_REQUEST, + session_id: 3, + timestamp: 789, + liveness_request: { + nonce: b4a.from('04', 'hex'), + signature: b4a.from('05', 'hex') + }, + capabilities: [] +}; + +const payloadLivenessResponse = { + type: NetworkOperationType.LIVENESS_RESPONSE, + session_id: 4, + timestamp: 101112, + liveness_response: { + nonce: b4a.from('06', 'hex'), + signature: b4a.from('07', 'hex'), + result: NetworkResultCode.OK + }, + capabilities: [] +}; + +const payloadBroadcastTransactionRequest = { + type: NetworkOperationType.BROADCAST_TRANSACTION_REQUEST, + session_id: 5, + timestamp: 131415, + broadcast_transaction_request: { + data: b4a.from('deadbeef', 'hex'), + nonce: b4a.from('08', 'hex'), + signature: b4a.from('09', 'hex') + }, + capabilities: ['cap:a'] +}; + +const payloadBroadcastTransactionResponse = { + type: NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE, + session_id: 6, + timestamp: 161718, + broadcast_transaction_response: { + nonce: b4a.from('0a', 'hex'), + signature: b4a.from('0b', 'hex'), + result: NetworkResultCode.OK + }, + capabilities: ['cap:b'] +}; + +export default { + payloadValidatorConnectionRequest, + payloadValidatorConnectionResponse, + payloadLivenessRequest, + payloadLivenessResponse, + payloadBroadcastTransactionRequest, + payloadBroadcastTransactionResponse, +}; + diff --git a/tests/fixtures/protobuf.fixtures.js b/tests/fixtures/protobuf.fixtures.js index f122ce70..c2c4c311 100644 --- a/tests/fixtures/protobuf.fixtures.js +++ b/tests/fixtures/protobuf.fixtures.js @@ -19,6 +19,22 @@ const validTransferOperation = { } } +const validPartialTransferOperation = { + type: OperationType.TRANSFER, + address: addressToBuffer('trac123z3gfpr2epjwww7ntm3m6ud2fhmq0tvts27p2f5mx3qkecsutlqfys769'), + tro: { + tx: b4a.from('c59f70942febb1de32fcb59febe84560416265d39f39b48fae676592910a98f4', 'hex'), + txv: b4a.from('eb59a3e756d1c9597e46b33bcea91e262f8f73e94c238bdf70854aa2e8c42608', 'hex'), + to: addressToBuffer('trac1mqktwme8fvklrds4hlhfy6lhmsu9qgfn3c3kuhz7c5zwjt8rc3dqj9tx7h'), + am: b4a.from('00000000000000015af1d78b58c40001', 'hex'), + in: b4a.from('863fef21f5146553b0396b2ee1a93a8dbfce240411b71ccdcfc504504a6b9b50', 'hex'), + is: b4a.from('06acd7faecd5159221259ebb1d7e98eccd7c6e2884de9de45097e6d9d8c37192602901c74dde6bb2f48f6f665edc84140627f6e9c42f774a0e9f55ef3b348e06', 'hex'), + va: null, + vn: null, + vs: null + } +} + const validTransactionOperation = { type: OperationType.TX, address: addressToBuffer('trac1c232xtkvyg08zyeurn7l0wrarc4y36fzq5vhcdsgkxe6hdpzuslsm63dw8', config.addressPrefix), @@ -37,6 +53,24 @@ const validTransactionOperation = { } }; +const validPartialTransactionOperation = { + type: OperationType.TX, + address: addressToBuffer('trac1c232xtkvyg08zyeurn7l0wrarc4y36fzq5vhcdsgkxe6hdpzuslsm63dw8'), + txo: { + tx: b4a.from('6fb7f6e7f6970477977080f2b46cc837d48605e67691d30bf7511a1417d17ed7', 'hex'), + txv: b4a.from('6fb7f6e7f6970477977080f2b46cc837d48605e67691d30bf7511a1417d17ed7', 'hex'), + iw: b4a.from('79ef7be837aa9fd8a446a120e1bc1e6bdd99fb5393dc4fa8299d9d5043a7cd98', 'hex'), + ch: b4a.from('6ee7b29ce494875c1ea0dc0f9c2997d1aeeb8d21c67809950e145822989c8b2e', 'hex'), + bs: b4a.from('f68bac445eee3d9276be46aef58328a543e4b7ecc2b5c98c387c1b3ca1a7e85d', 'hex'), + mbs: b4a.from('5f3b9a6a516066de365e5e75a7ac0feb55ab7cd4a29facbb028a047fc3f3956e', 'hex'), + in: b4a.from('8bcef53a043f42ac7c17344f0c0d56af5b335e412d4042124f27733911169e4f', 'hex'), + is: b4a.from('d8626ea0552bf302921de3536e877796ef131368c9854119660c9c77a4196d4735d60bb87c6a89bbff7d5f8d72a70610d6ee73d62bc5144874cdf23f88e28a05', 'hex'), + va: null, + vn: null, + vs: null + } +}; + const validAddIndexer = { type: OperationType.ADD_INDEXER, address: addressToBuffer('trac1jnafrn8xl3c59s4ml6jusufp2vzm6egkyenrcqvd907j8c9v5j2qnx7wvt', config.addressPrefix), @@ -201,6 +235,50 @@ const validBalanceInitOperation = { } }; +const validDisableInitialization = { + type: OperationType.DISABLE_INITIALIZATION, + address: addressToBuffer('trac18qq7h503y3326v6msgvq0jwc0e8jp4t4q53z9p9jvd98arj7mtpqfac04p'), + cao: { + tx: b4a.from('1bd4f96adeffba9c04943a82993c5b19660c3a5f572620d82a67464f381640e2', 'hex'), + txv: b4a.from('f24e61cf7941256b080be2133bccb520414c78021215edfcb781622da526c414', 'hex'), + iw: b4a.from('71c53657a8738b48772f0940398d4f4b01dc56cb32cd2fd84c30359f0cbb08f1', 'hex'), + in: b4a.from('0ad7fe36a35a27ea4df932b800200823a97d4db31bca247f43ad7523b0493645', 'hex'), + is: b4a.from('5b534be7a374148962c271d194c26cf5b1ad705ab218a87709a33fe74f9d1b811772447c939b17b2f803e3da7648f49b666b929fbb20e458ced952f147162c08', 'hex') + } +}; + +const validPartialBootstrapDeployment = { + type: OperationType.BOOTSTRAP_DEPLOYMENT, + address: addressToBuffer('trac1c232xtkvyg08zyeurn7l0wrarc4y36fzq5vhcdsgkxe6hdpzuslsm63dw8'), + bdo: { + tx: b4a.from('6fb7f6e7f6970477977080f2b46cc837d48605e67691d30bf7511a1417d17ed7', 'hex'), + txv: b4a.from('eb59a3e756d1c9597e46b33bcea91e262f8f73e94c238bdf70854aa2e8c42608', 'hex'), + bs: b4a.from('f68bac445eee3d9276be46aef58328a543e4b7ecc2b5c98c387c1b3ca1a7e85d', 'hex'), + ic: b4a.from('79ef7be837aa9fd8a446a120e1bc1e6bdd99fb5393dc4fa8299d9d5043a7cd98', 'hex'), + in: b4a.from('8bcef53a043f42ac7c17344f0c0d56af5b335e412d4042124f27733911169e4f', 'hex'), + is: b4a.from('d8626ea0552bf302921de3536e877796ef131368c9854119660c9c77a4196d4735d60bb87c6a89bbff7d5f8d72a70610d6ee73d62bc5144874cdf23f88e28a05', 'hex'), + va: null, + vn: null, + vs: null + } +}; + +const validCompleteBootstrapDeployment = { + type: OperationType.BOOTSTRAP_DEPLOYMENT, + address: addressToBuffer('trac1c232xtkvyg08zyeurn7l0wrarc4y36fzq5vhcdsgkxe6hdpzuslsm63dw8'), + bdo: { + tx: b4a.from('6fb7f6e7f6970477977080f2b46cc837d48605e67691d30bf7511a1417d17ed7', 'hex'), + txv: b4a.from('eb59a3e756d1c9597e46b33bcea91e262f8f73e94c238bdf70854aa2e8c42608', 'hex'), + bs: b4a.from('f68bac445eee3d9276be46aef58328a543e4b7ecc2b5c98c387c1b3ca1a7e85d', 'hex'), + ic: b4a.from('79ef7be837aa9fd8a446a120e1bc1e6bdd99fb5393dc4fa8299d9d5043a7cd98', 'hex'), + in: b4a.from('8bcef53a043f42ac7c17344f0c0d56af5b335e412d4042124f27733911169e4f', 'hex'), + is: b4a.from('d8626ea0552bf302921de3536e877796ef131368c9854119660c9c77a4196d4735d60bb87c6a89bbff7d5f8d72a70610d6ee73d62bc5144874cdf23f88e28a05', 'hex'), + va: addressToBuffer('trac1xvqvlzx4w2q2pfqrmycew87kq4rv0q0cewxk68ddvddgk2xm09cqvpc4jc'), + vn: b4a.from('9027192c6de13b683bc0c0fbcfe09c4e55d47c12c46b122d988f06c282a4be5e', 'hex'), + vs: b4a.from('8fb8a3ba30e00c347bca5a8554c47e167f63b248c87e1ea5532eebbad1bc036184fe8872ff65a9e63acfee68d2213a187466c13ff6687d3ab57e5209abd4fb01', 'hex') + } +}; + const invalidPayloads = [ null, @@ -286,12 +364,15 @@ const invalidPayloadWithMultipleOneOfKeys = { export default { validTransactionOperation, + validPartialTransactionOperation, validAddIndexer, validRemoveIndexer, validAppendWhitelist, validBanValidator, validAddAdmin, + validDisableInitialization, validTransferOperation, + validPartialTransferOperation, validBalanceInitOperation, validPartialAddWriter, validCompleteAddWriter, @@ -299,6 +380,8 @@ export default { validPartialAdminRecovery, validCompleteRemoveWriter, validPartialRemoveWriter, + validPartialBootstrapDeployment, + validCompleteBootstrapDeployment, invalidPayloads, invalidPayloadWithMultipleOneOfKeys }; diff --git a/tests/helpers/config.js b/tests/helpers/config.js index 7def1e56..3a2d33ae 100644 --- a/tests/helpers/config.js +++ b/tests/helpers/config.js @@ -1,3 +1,3 @@ import { createConfig, ENV } from '../../src/config/env.js' -export const config = createConfig(ENV.DEVELOPMENT, {}) +export const config = createConfig(ENV.MAINNET, {}) diff --git a/tests/helpers/setupApplyTests.js b/tests/helpers/setupApplyTests.js index b1b81889..5a31be3e 100644 --- a/tests/helpers/setupApplyTests.js +++ b/tests/helpers/setupApplyTests.js @@ -3,16 +3,13 @@ import {generateMnemonic, mnemonicToSeed} from 'bip39-mnemonic'; import b4a from 'b4a' import PeerWallet from "trac-wallet" import path from 'path'; -import CompleteStateMessageOperations from '../../src/messages/completeStateMessages/CompleteStateMessageOperations.js' -import PartialStateMessageOperations from '../../src/messages/partialStateMessages/PartialStateMessageOperations.js'; import {MainSettlementBus} from '../../src/index.js' import { createConfig, ENV } from '../../src/config/env.js' import fileUtils from '../../src/utils/fileUtils.js' import {EntryType} from '../../src/utils/constants.js'; import {sleep} from '../../src/utils/helpers.js' import {formatIndexersEntry} from '../../src/utils/helpers.js'; -import CompleteStateMessageBuilder from '../../src/messages/completeStateMessages/CompleteStateMessageBuilder.js' -import CompleteStateMessageDirector from '../../src/messages/completeStateMessages/CompleteStateMessageDirector.js' +import { applyStateMessageFactory } from "../../src/messages/state/applyStateMessageFactory.js"; import { safeEncodeApplyOperation } from "../../src/utils/protobuf/operationHelpers.js" import { $TNK } from '../../src/core/state/utils/balance.js'; import { EventType } from '../../src/utils/constants.js'; @@ -69,14 +66,13 @@ export const tick = () => new Promise(resolve => setImmediate(resolve)); export async function fundPeer(admin, toFund, amount) { const txValidity = await admin.msb.state.getIndexerSequenceState() - const director = new CompleteStateMessageDirector(); - director.builder = new CompleteStateMessageBuilder(admin.wallet, admin.config); - const payload = await director.buildBalanceInitializationMessage( - admin.wallet.address, - toFund.wallet.address, - amount, - txValidity - ); + const payload = await applyStateMessageFactory(admin.wallet, admin.config) + .buildCompleteBalanceInitializationMessage( + admin.wallet.address, + toFund.wallet.address, + amount, + txValidity + ); await admin.msb.state.append(safeEncodeApplyOperation(payload)); await tick() @@ -124,10 +120,10 @@ export async function initMsbAdmin(keyPair, temporaryDirectory, options = {}) { export async function setupMsbAdmin(keyPair, temporaryDirectory, options = {}) { const admin = await initMsbAdmin(keyPair, temporaryDirectory, options); const txValidity = await admin.msb.state.getIndexerSequenceState(); - const addAdminMessage = await new CompleteStateMessageOperations(admin.wallet, admin.config) - .assembleAddAdminMessage(admin.msb.state.writingKey, txValidity); + const payload = await applyStateMessageFactory(admin.wallet, admin.config) + .buildCompleteAddAdminMessage(admin.wallet.address, admin.msb.state.writingKey, txValidity); - await admin.msb.state.append(addAdminMessage); + await admin.msb.state.append(safeEncodeApplyOperation(payload)); await tick(); return admin; } @@ -137,22 +133,25 @@ export async function setupNodeAsWriter(admin, writerCandidate) { await setupWhitelist(admin, [writerCandidate.wallet.address]); // ensure if is whitelisted const validity = await admin.msb.getIndexerSequenceState() - const req = await new PartialStateMessageOperations(writerCandidate.wallet, admin.config) - .assembleAddWriterMessage( + const req = await applyStateMessageFactory(writerCandidate.wallet, admin.config) + .buildPartialAddWriterMessage( + writerCandidate.wallet.address, b4a.toString(writerCandidate.msb.state.writingKey, 'hex'), - b4a.toString(validity, 'hex')); + b4a.toString(validity, 'hex'), + 'json' + ); await waitWritable(admin, writerCandidate, async () => { - const raw = await new CompleteStateMessageOperations(admin.wallet, admin.config) - .assembleAddWriterMessage( + const payload = await applyStateMessageFactory(admin.wallet, admin.config) + .buildCompleteAddWriterMessage( admin.wallet.address, b4a.from(req.rao.tx, 'hex'), b4a.from(req.rao.txv, 'hex'), b4a.from(req.rao.iw, 'hex'), b4a.from(req.rao.in, 'hex'), b4a.from(req.rao.is, 'hex') - ) - await admin.msb.state.append(raw) + ); + await admin.msb.state.append(safeEncodeApplyOperation(payload)) }) return writerCandidate; @@ -172,22 +171,25 @@ export async function promoteToWriter(admin, writerCandidate) { isIndexer: false, }) const validity = await admin.msb.state.getIndexerSequenceState() - const req = await new PartialStateMessageOperations(writerCandidate.wallet, writerCandidate.config) - .assembleAddWriterMessage( + const req = await applyStateMessageFactory(writerCandidate.wallet, writerCandidate.config) + .buildPartialAddWriterMessage( + writerCandidate.wallet.address, b4a.toString(writerCandidate.msb.state.writingKey, 'hex'), - b4a.toString(validity, 'hex')); + b4a.toString(validity, 'hex'), + 'json' + ); await waitWritable(writerCandidate, writerCandidate, async () => { - const raw = await new CompleteStateMessageOperations(admin.wallet, admin.config) - .assembleAddWriterMessage( + const payload = await applyStateMessageFactory(admin.wallet, admin.config) + .buildCompleteAddWriterMessage( req.address, b4a.from(req.rao.tx, 'hex'), b4a.from(req.rao.txv, 'hex'), b4a.from(req.rao.iw, 'hex'), b4a.from(req.rao.in, 'hex'), b4a.from(req.rao.is, 'hex') - ) - await admin.msb.state.append(raw) + ); + await admin.msb.state.append(safeEncodeApplyOperation(payload)) }) return writerCandidate; @@ -206,10 +208,10 @@ export async function setupMsbWriter(admin, peerName, peerKeyPair, temporaryDire export async function setupMsbIndexer(indexerCandidate, admin) { try { const validity = await admin.msb.state.getIndexerSequenceState() - const req = await new CompleteStateMessageOperations(admin.wallet, admin.config) - .assembleAddIndexerMessage(indexerCandidate.wallet.address, validity); + const payload = await applyStateMessageFactory(admin.wallet, admin.config) + .buildCompleteAddIndexerMessage(admin.wallet.address, indexerCandidate.wallet.address, validity); - await admin.msb.state.append(req); + await admin.msb.state.append(safeEncodeApplyOperation(payload)); await tick(); // wait for the request to be processed const isIndexer = async () => { @@ -260,10 +262,10 @@ export async function setupWhitelist(admin, whitelistAddresses) { fileUtils.readAddressesFromWhitelistFile = async () => whitelistAddresses; const validity = await admin.msb.state.getIndexerSequenceState() for (const address of whitelistAddresses) { - const msg = await new CompleteStateMessageOperations(admin.wallet, admin.config) - .assembleAppendWhitelistMessages(validity, address); + const payload = await applyStateMessageFactory(admin.wallet, admin.config) + .buildCompleteAppendWhitelistMessage(admin.wallet.address, address, validity); - await admin.msb.state.append(msg); + await admin.msb.state.append(safeEncodeApplyOperation(payload)); await sleep(100) } @@ -314,15 +316,17 @@ export async function initDirectoryStructure(peerName, keyPair, temporaryDirecto export const deployExternalBootstrap = async (writer, externalNode) => { const externalBootstrap = randomBytes(32).toString('hex'); const txValidity = await writer.msb.state.getIndexerSequenceState(); - const payload = await new PartialStateMessageOperations(externalNode.msb.wallet, admin.config) - .assembleBootstrapDeploymentMessage( + const payload = await applyStateMessageFactory(externalNode.msb.wallet, admin.config) + .buildPartialBootstrapDeploymentMessage( + externalNode.msb.wallet.address, externalBootstrap, randomBytes(32).toString('hex'), - txValidity.toString('hex') + txValidity.toString('hex'), + 'json' ); - const raw = await new CompleteStateMessageOperations(writer.msb.wallet, admin.config) - .assembleCompleteBootstrapDeployment( + const rawPayload = await applyStateMessageFactory(writer.msb.wallet, admin.config) + .buildCompleteBootstrapDeploymentMessage( payload.address, b4a.from(payload.bdo.tx, 'hex'), b4a.from(payload.bdo.txv, 'hex'), @@ -331,7 +335,7 @@ export const deployExternalBootstrap = async (writer, externalNode) => { b4a.from(payload.bdo.in, 'hex'), b4a.from(payload.bdo.is, 'hex'), ) - await writer.msb.state.base.append(raw) + await writer.msb.state.base.append(safeEncodeApplyOperation(rawPayload)) await tick() await waitForHash(writer, payload.bdo.tx) return externalBootstrap @@ -353,17 +357,19 @@ export const generatePostTx = async (writer, externalNode, externalContractBoots const contentHash = await PeerWallet.blake3(JSON.stringify(testObj)); const validity = await writer.msb.state.getIndexerSequenceState() - const tx = await new PartialStateMessageOperations(externalNode.wallet, admin.config) - .assembleTransactionOperationMessage( + const tx = await applyStateMessageFactory(externalNode.wallet, admin.config) + .buildPartialTransactionOperationMessage( + externalNode.wallet.address, peerWriterKey, b4a.toString(validity, 'hex'), b4a.toString(contentHash, 'hex'), externalContractBootstrap, - b4a.toString(writer.msb.bootstrap, 'hex') + b4a.toString(writer.msb.bootstrap, 'hex'), + 'json' ) - const postTx = await new CompleteStateMessageOperations(writer.wallet, admin.config) - .assembleCompleteTransactionOperationMessage( + const postTxPayload = await applyStateMessageFactory(writer.wallet, admin.config) + .buildCompleteTransactionOperationMessage( tx.address, b4a.from(tx.txo.tx, 'hex'), b4a.from(tx.txo.txv, 'hex'), @@ -375,6 +381,7 @@ export const generatePostTx = async (writer, externalNode, externalContractBoots b4a.from(tx.txo.mbs, 'hex') ); + const postTx = safeEncodeApplyOperation(postTxPayload); return { postTx, txHash: tx.txo.tx }; } diff --git a/tests/integration/apply/addAdmin/addAdminBasic.test.js b/tests/integration/apply/addAdmin/addAdminBasic.test.js index b8ac5609..dcab09ec 100644 --- a/tests/integration/apply/addAdmin/addAdminBasic.test.js +++ b/tests/integration/apply/addAdmin/addAdminBasic.test.js @@ -4,7 +4,8 @@ import { tryToSyncWriters } from '../../../helpers/setupApplyTests.js'; import {randomBytes} from '../../../helpers/setupApplyTests.js'; -import CompleteStateMessageOperations from '../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../src/utils/protobuf/operationHelpers.js'; import { config } from '../../../helpers/config.js'; import {testKeyPair1} from '../../../fixtures/apply.fixtures.js'; import b4a from 'b4a'; @@ -18,11 +19,14 @@ let randomChannel; const sendAddAdmin = async (invoker) => { const validity = b4a.from(await admin.msb.state.getIndexerSequenceState(), 'hex') - const addAdminMessage = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleAddAdminMessage( - admin.msb.state.writingKey, - validity - ); + const addAdminMessage = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteAddAdminMessage( + admin.wallet.address, + admin.msb.state.writingKey, + validity + ) + ); // add admin to base await invoker.msb.state.append(addAdminMessage); // Send `add admin` request to apply function diff --git a/tests/integration/apply/addAdmin/addAdminRecovery.test.js b/tests/integration/apply/addAdmin/addAdminRecovery.test.js index 07a8d2eb..43fa7ef3 100644 --- a/tests/integration/apply/addAdmin/addAdminRecovery.test.js +++ b/tests/integration/apply/addAdmin/addAdminRecovery.test.js @@ -5,8 +5,8 @@ import { } from '../../../helpers/setupApplyTests.js'; import {randomBytes} from '../../../helpers/setupApplyTests.js'; -import CompleteStateMessageOperations from '../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; -import PartialStateMessageOperations from '../../../../src/messages/partialStateMessages/PartialStateMessageOperations.js' +import { applyStateMessageFactory } from '../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../src/utils/protobuf/operationHelpers.js'; import {testKeyPair1, testKeyPair2, testKeyPair3, testKeyPair4} from '../../../fixtures/apply.fixtures.js'; import b4a from 'b4a'; import { decode as decodeAdmin } from '../../../../src/core/state/utils/adminEntry.js'; @@ -88,22 +88,24 @@ test('Apply function addAdmin for recovery - happy path', async (k) => { await newAdmin.msb.ready(); await newAdmin.msb.state.append(null); const validity = b4a.toString(await newAdmin.msb.state.getIndexerSequenceState(), 'hex') - const addAdminMessage = await new PartialStateMessageOperations(newAdmin.wallet, config) - .assembleAdminRecoveryMessage( + const addAdminMessage = await applyStateMessageFactory(newAdmin.wallet, config) + .buildPartialAdminRecoveryMessage( + newAdmin.wallet.address, b4a.toString(newAdmin.msb.state.writingKey, 'hex'), - validity + validity, + 'json' ); - const rawTx = await new CompleteStateMessageOperations(writer.wallet, config) - .assembleAdminRecoveryMessage( + const rawPayload = await applyStateMessageFactory(writer.wallet, config) + .buildCompleteAdminRecoveryMessage( addAdminMessage.address, b4a.from(addAdminMessage.rao.tx, 'hex'), b4a.from(addAdminMessage.rao.txv, 'hex'), b4a.from(addAdminMessage.rao.iw, 'hex'), b4a.from(addAdminMessage.rao.in, 'hex'), b4a.from(addAdminMessage.rao.is, 'hex') - ) - await writer.msb.state.append(rawTx) + ); + await writer.msb.state.append(safeEncodeApplyOperation(rawPayload)) await tryToSyncWriters(writer, indexer1, indexer2, newAdmin); const adminEntryAfter = decodeAdmin(await writer.msb.state.get(EntryType.ADMIN), config.addressPrefix); // check if the admin entry was added successfully in the base k.ok(adminEntryAfter, 'Result should not be null'); diff --git a/tests/integration/apply/addIndexer.test.js b/tests/integration/apply/addIndexer.test.js index 22f88de2..f593a9b6 100644 --- a/tests/integration/apply/addIndexer.test.js +++ b/tests/integration/apply/addIndexer.test.js @@ -1,5 +1,6 @@ import {test, hook, solo} from 'brittle'; -import CompleteStateMessageOperations from '../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../src/utils/protobuf/operationHelpers.js'; import { initTemporaryDirectory, removeTemporaryDirectory, @@ -55,8 +56,10 @@ test('handleApplyAddIndexerOperation (apply) - Append addIndexer payload into th // indexer3 is just a writer. const oldIndexersEntry = await admin.msb.state.getIndexersEntry(); const validity = await admin.msb.state.getIndexerSequenceState() - const assembledAddIndexerMessage = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleAddIndexerMessage(indexer1.wallet.address, validity); + const assembledAddIndexerMessage = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteAddIndexerMessage(admin.wallet.address, indexer1.wallet.address, validity) + ); await waitIndexer(indexer1, async () => await admin.msb.state.append(assembledAddIndexerMessage)) await waitForNodeState(admin, indexer1.wallet.address, { @@ -82,11 +85,14 @@ test('handleApplyAddIndexerOperation (apply) - Append addIndexer payload into th // reader2 is just a reading node. // indexer3 is just a writer. const indexersEntryBefore = await admin.msb.state.getIndexersEntry(); - const assembledAddIndexerMessage = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleAddIndexerMessage( - indexer2.wallet.address, - await admin.msb.state.getIndexerSequenceState() - ); + const assembledAddIndexerMessage = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteAddIndexerMessage( + admin.wallet.address, + indexer2.wallet.address, + await admin.msb.state.getIndexerSequenceState() + ) + ); await waitIndexer(indexer2, async () => await admin.msb.state.append(assembledAddIndexerMessage)) await waitForNodeState(admin, indexer2.wallet.address, { @@ -99,9 +105,13 @@ test('handleApplyAddIndexerOperation (apply) - Append addIndexer payload into th await tryToSyncWriters(admin, indexer1, indexer2); const adminSignedLengthBefore = admin.msb.state.getSignedLength(); - const reqAddIndexerMessageAgain = await new CompleteStateMessageOperations(admin.wallet, config).assembleAddIndexerMessage( - indexer2.wallet.address, - await admin.msb.state.getIndexerSequenceState() + const reqAddIndexerMessageAgain = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteAddIndexerMessage( + admin.wallet.address, + indexer2.wallet.address, + await admin.msb.state.getIndexerSequenceState() + ) ); await admin.msb.state.append(reqAddIndexerMessageAgain); @@ -135,8 +145,10 @@ test('handleApplyAddIndexerOperation (apply) - Append addIndexer payload into th // indexer3 is just a writer. const indexersEntryBefore = await indexer1.msb.state.getIndexersEntry(); const validity = await admin.msb.state.getIndexerSequenceState() - const reqAddReader = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleAddIndexerMessage(reader1.wallet.address, validity); + const reqAddReader = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteAddIndexerMessage(admin.wallet.address, reader1.wallet.address, validity) + ); const adminSignedLengthBefore = admin.msb.state.getSignedLength() const indexer1SignedLengthBefore = indexer1.msb.state.getSignedLength(); @@ -169,10 +181,14 @@ test('handleApplyAddIndexerOperation (apply) - Append addIndexer payload into th const indexersEntryBeforeWhitelist = await admin.msb.state.getIndexersEntry(); const adminSignedLengthBefore = admin.msb.state.getSignedLength(); const validity = await admin.msb.state.getIndexerSequenceState() - const reqAddIndexer2 = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleAddIndexerMessage( - reader2.wallet.address, - validity); + const reqAddIndexer2 = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteAddIndexerMessage( + admin.wallet.address, + reader2.wallet.address, + validity + ) + ); await admin.msb.state.append(reqAddIndexer2); await tryToSyncWriters(admin, indexer1, indexer2); @@ -200,10 +216,14 @@ test('handleApplyAddIndexerOperation (apply) - Append addIndexer payload into th const adminSignedLengthBefore = admin.msb.state.getSignedLength(); const validity = await admin.msb.state.getIndexerSequenceState() - const assembledAddIndexerMessage = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleAddIndexerMessage( - indexer3.wallet.address, - validity); + const assembledAddIndexerMessage = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteAddIndexerMessage( + admin.wallet.address, + indexer3.wallet.address, + validity + ) + ); await writer.msb.state.append(assembledAddIndexerMessage); await tryToSyncWriters(admin, writer, indexer1, indexer2); diff --git a/tests/integration/apply/addWhitelist.test.js b/tests/integration/apply/addWhitelist.test.js index 5430e508..b9e36b41 100644 --- a/tests/integration/apply/addWhitelist.test.js +++ b/tests/integration/apply/addWhitelist.test.js @@ -3,7 +3,8 @@ import b4a from 'b4a'; import { setupMsbAdmin, initTemporaryDirectory, removeTemporaryDirectory, randomBytes } from '../../helpers/setupApplyTests.js'; import { testKeyPair1, testKeyPair2 } from '../../fixtures/apply.fixtures.js'; import fileUtils from '../../../src/utils/fileUtils.js'; -import CompleteStateMessageOperations from '../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../src/utils/protobuf/operationHelpers.js'; import { address as addressApi } from 'trac-crypto-api'; import { config } from '../../helpers/config.js'; @@ -32,8 +33,10 @@ hook('Initialize admin node for addWhitelist tests', async () => { test('Apply function addWhitelist - happy path', async (t) => { const validity = await admin.msb.state.getIndexerSequenceState(); - const payload = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleAppendWhitelistMessages(validity, address); + const payload = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteAppendWhitelistMessage(admin.wallet.address, address, validity) + ); await admin.msb.state.append(payload); const isWhitelisted = await admin.msb.state.isAddressWhitelisted(address); diff --git a/tests/integration/apply/addWriter.test.js b/tests/integration/apply/addWriter.test.js index a353de6f..82f26ea9 100644 --- a/tests/integration/apply/addWriter.test.js +++ b/tests/integration/apply/addWriter.test.js @@ -23,29 +23,32 @@ import { testKeyPair5, testKeyPair6 } from '../../fixtures/apply.fixtures.js'; -import PartialStateMessageOperations from "../../../src/messages/partialStateMessages/PartialStateMessageOperations.js"; -import CompleteStateMessageOperations from '../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../src/utils/protobuf/operationHelpers.js'; import {ZERO_WK} from '../../../src/utils/buffer.js'; import { $TNK } from '../../../src/core/state/utils/balance.js'; import { config } from '../../helpers/config.js'; const sendAddWriter = async (invoker, broadcaster) => { const validity = await invoker.msb.state.getIndexerSequenceState() - const req = await new PartialStateMessageOperations(invoker.wallet, config) - .assembleAddWriterMessage( + const req = await applyStateMessageFactory(invoker.wallet, config) + .buildPartialAddWriterMessage( + invoker.wallet.address, b4a.toString(invoker.msb.state.writingKey, 'hex'), - b4a.toString(validity, 'hex')); + b4a.toString(validity, 'hex'), + 'json' + ); - const raw = await new CompleteStateMessageOperations(broadcaster.wallet, config) - .assembleAddWriterMessage( + const rawPayload = await applyStateMessageFactory(broadcaster.wallet, config) + .buildCompleteAddWriterMessage( req.address, b4a.from(req.rao.tx, 'hex'), b4a.from(req.rao.txv, 'hex'), b4a.from(req.rao.iw, 'hex'), b4a.from(req.rao.in, 'hex'), b4a.from(req.rao.is, 'hex') - ) - return await broadcaster.msb.state.append(raw) + ); + return await broadcaster.msb.state.append(safeEncodeApplyOperation(rawPayload)) } let admin, writer1, writer2, writer3, writer4, indexer1, tmpDirectory; @@ -146,21 +149,24 @@ test('handleApplyAddWriterOperation (apply) - Append addWriter payload into the const signedLengthWriter1Before = writer1.msb.state.getSignedLength(); const validity = await writer3.msb.state.getIndexerSequenceState() - const req = await new PartialStateMessageOperations(writer3.wallet, config) - .assembleAddWriterMessage( + const req = await applyStateMessageFactory(writer3.wallet, config) + .buildPartialAddWriterMessage( + writer3.wallet.address, b4a.toString(ZERO_WK, 'hex'), - b4a.toString(validity, 'hex')); + b4a.toString(validity, 'hex'), + 'json' + ); - const raw = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleAddWriterMessage( + const rawPayload = await applyStateMessageFactory(admin.wallet, config) + .buildCompleteAddWriterMessage( admin.wallet.address, b4a.from(req.rao.tx, 'hex'), b4a.from(req.rao.txv, 'hex'), b4a.from(req.rao.iw, 'hex'), b4a.from(req.rao.in, 'hex'), b4a.from(req.rao.is, 'hex') - ) - await admin.msb.state.append(raw) + ); + await admin.msb.state.append(safeEncodeApplyOperation(rawPayload)) await tryToSyncWriters(writer3, admin, writer1, writer2, indexer1); const result = await writer1.msb.state.getNodeEntry(writer3.wallet.address); diff --git a/tests/integration/apply/banValidator.test.js b/tests/integration/apply/banValidator.test.js index 6fad8f74..7384cf44 100644 --- a/tests/integration/apply/banValidator.test.js +++ b/tests/integration/apply/banValidator.test.js @@ -9,7 +9,8 @@ import { tryToSyncWriters } from '../../helpers/setupApplyTests.js'; import { randomBytes } from '../../helpers/setupApplyTests.js'; -import CompleteStateMessageOperations from '../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../src/utils/protobuf/operationHelpers.js'; import { testKeyPair1, testKeyPair2, testKeyPair3, testKeyPair4 } from '../../fixtures/apply.fixtures.js'; import { sleep } from '../../../src/utils/helpers.js'; @@ -41,8 +42,10 @@ hook('Initialize nodes for banValidator tests', async () => { test('handleApplyBanValidatorOperation (apply) - Append banValidator payload - ban indexer', async t => { const validity = await admin.msb.state.getIndexerSequenceState() - const assembledBanWriter = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleBanWriterMessage(indexer.wallet.address, validity); + const assembledBanWriter = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteBanWriterMessage(admin.wallet.address, indexer.wallet.address, validity) + ); await admin.msb.state.append(assembledBanWriter); await tryToSyncWriters(admin, indexer, writer1, writer2); @@ -56,8 +59,10 @@ test('handleApplyBanValidatorOperation (apply) - Append banValidator payload - b test('handleApplyBanValidatorOperation (apply) - Append banValidator payload into the base by non-admin node', async t => { const validity = await admin.msb.state.getIndexerSequenceState() - const assembledBanWriter = await new CompleteStateMessageOperations(writer1.wallet, config) - .assembleBanWriterMessage(writer2.wallet.address, validity); + const assembledBanWriter = safeEncodeApplyOperation( + await applyStateMessageFactory(writer1.wallet, config) + .buildCompleteBanWriterMessage(writer1.wallet.address, writer2.wallet.address, validity) + ); await writer1.msb.state.append(assembledBanWriter); await sleep(5000); // wait for both peers to sync state await tryToSyncWriters(admin); @@ -71,8 +76,10 @@ test('handleApplyBanValidatorOperation (apply) - Append banValidator payload int test('handleApplyBanValidatorOperation (apply) - Append banValidator payload into the base - happy path', async t => { const validity = await admin.msb.state.getIndexerSequenceState() - const assembledBanWriter = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleBanWriterMessage(writer1.wallet.address, validity); + const assembledBanWriter = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteBanWriterMessage(admin.wallet.address, writer1.wallet.address, validity) + ); await admin.msb.state.append(assembledBanWriter); await sleep(5000); // wait for both peers to sync state @@ -85,16 +92,20 @@ test('handleApplyBanValidatorOperation (apply) - Append banValidator payload int test('handleApplyBanValidatorOperation (apply) - Append banValidator payload into the base - idempotence', async t => { const validity = await admin.msb.state.getIndexerSequenceState() - const assembledBanWriter = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleBanWriterMessage(writer2.wallet.address, validity); + const assembledBanWriter = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteBanWriterMessage(admin.wallet.address, writer2.wallet.address, validity) + ); await admin.msb.state.append(assembledBanWriter); await sleep(5000); // wait for both peers to sync state const nodeInfo = await writer2.msb.state.getNodeEntry(writer2.wallet.address); const validity2 = await admin.msb.state.getIndexerSequenceState() - const assembledBanWriter2 = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleBanWriterMessage(writer2.wallet.address, validity2); + const assembledBanWriter2 = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteBanWriterMessage(admin.wallet.address, writer2.wallet.address, validity2) + ); await admin.msb.state.append(assembledBanWriter2); await sleep(5000); // wait for both peers to sync state diff --git a/tests/integration/apply/removeIndexer.test.js b/tests/integration/apply/removeIndexer.test.js index 4e3306f2..618f3d1b 100644 --- a/tests/integration/apply/removeIndexer.test.js +++ b/tests/integration/apply/removeIndexer.test.js @@ -1,6 +1,7 @@ import {test, hook} from '../../helpers/wrapper.js'; -import CompleteStateMessageOperations from '../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../src/utils/protobuf/operationHelpers.js'; import { config } from '../../helpers/config.js'; import { initTemporaryDirectory, @@ -42,8 +43,10 @@ test('handleApplyRemoveIndexerOperation (apply) - Append removeIndexer payload i // writer is already a writer const indexersEntryBefore = await writer.msb.state.getIndexersEntry(); const validity = await admin.msb.state.getIndexerSequenceState() - const assembledRemoveIndexerMessage = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleRemoveIndexerMessage(indexer1.wallet.address, validity); + const assembledRemoveIndexerMessage = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteRemoveIndexerMessage(admin.wallet.address, indexer1.wallet.address, validity) + ); await admin.msb.state.append(assembledRemoveIndexerMessage); await tryToSyncWriters(admin, indexer1, indexer2); await waitForNodeState(indexer1, indexer1.wallet.address, { @@ -73,8 +76,10 @@ test('handleApplyRemoveIndexerOperation (apply) - Append removeIndexer payload i const indexersEntryBefore = await indexer1.msb.state.getIndexersEntry(); const validity = await admin.msb.state.getIndexerSequenceState() - const assembledRemoveIndexerMessage = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleRemoveIndexerMessage(indexer1.wallet.address, validity); + const assembledRemoveIndexerMessage = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteRemoveIndexerMessage(admin.wallet.address, indexer1.wallet.address, validity) + ); await admin.msb.state.append(assembledRemoveIndexerMessage); await tryToSyncWriters(admin, indexer2, writer); @@ -103,8 +108,10 @@ test('handleApplyAddIndexerOperation (apply) - Append removeIndexer payload into const indexer2SignedLengthBefore = indexer2.msb.state.getSignedLength(); const validity = await admin.msb.state.getIndexerSequenceState() - const assembledRemoveIndexerMessage = await new CompleteStateMessageOperations(admin.wallet, config) - .assembleRemoveIndexerMessage(indexer2.wallet.address, validity); + const assembledRemoveIndexerMessage = safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteRemoveIndexerMessage(admin.wallet.address, indexer2.wallet.address, validity) + ); await writer.msb.state.append(assembledRemoveIndexerMessage); await tryToSyncWriters(admin, indexer2, writer); diff --git a/tests/integration/apply/removeWriter.test.js b/tests/integration/apply/removeWriter.test.js index e419a6ef..9bedcdf3 100644 --- a/tests/integration/apply/removeWriter.test.js +++ b/tests/integration/apply/removeWriter.test.js @@ -19,31 +19,33 @@ import { testKeyPair5, testKeyPair6 } from '../../fixtures/apply.fixtures.js'; -import CompleteStateMessageOperations from '../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; -import PartialStateMessageOperations from '../../../src/messages/partialStateMessages/PartialStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../src/utils/protobuf/operationHelpers.js'; import { config } from '../../helpers/config.js'; let admin, writer1, writer2, writer3, writer4, indexer, tmpDirectory; const sendRemoveWriter = async (invoker, broadcaster) => { const validity = await invoker.msb.state.getIndexerSequenceState() - const writerRemoval = await new PartialStateMessageOperations(invoker.wallet, config) - .assembleRemoveWriterMessage( + const writerRemoval = await applyStateMessageFactory(invoker.wallet, config) + .buildPartialRemoveWriterMessage( + invoker.wallet.address, b4a.toString(invoker.msb.state.writingKey, 'hex'), - b4a.toString(validity, 'hex') + b4a.toString(validity, 'hex'), + 'json' ); - const raw = await new CompleteStateMessageOperations(broadcaster.wallet, config) - .assembleRemoveWriterMessage( + const rawPayload = await applyStateMessageFactory(broadcaster.wallet, config) + .buildCompleteRemoveWriterMessage( writerRemoval.address, b4a.from(writerRemoval.rao.tx, 'hex'), b4a.from(writerRemoval.rao.txv, 'hex'), b4a.from(writerRemoval.rao.iw, 'hex'), b4a.from(writerRemoval.rao.in, 'hex'), b4a.from(writerRemoval.rao.is, 'hex'), - ) + ); - return await broadcaster.msb.state.append(raw) + return await broadcaster.msb.state.append(safeEncodeApplyOperation(rawPayload)) } hook('Initialize nodes for removeWriter tests', async () => { diff --git a/tests/integration/apply/transfer.test.js b/tests/integration/apply/transfer.test.js index 8cd51610..46e0edba 100644 --- a/tests/integration/apply/transfer.test.js +++ b/tests/integration/apply/transfer.test.js @@ -14,18 +14,25 @@ import { testKeyPair3, testKeyPair4 } from '../../fixtures/apply.fixtures.js'; -import PartialStateMessageOperations from "../../../src/messages/partialStateMessages/PartialStateMessageOperations.js"; -import CompleteStateMessageOperations from '../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../src/utils/protobuf/operationHelpers.js'; import { $TNK } from '../../../src/core/state/utils/balance.js'; import { config } from '../../helpers/config.js'; const buildTransfer = async (admin, from, to, amount) => { const txValidity = await from.msb.state.getIndexerSequenceState() - const tx = await new PartialStateMessageOperations(from.wallet, config) - .assembleTransferOperationMessage(to.wallet.address, b4a.toString(amount, 'hex'), b4a.toString(txValidity, 'hex')) + const tx = await applyStateMessageFactory(from.wallet, config) + .buildPartialTransferOperationMessage( + from.wallet.address, + to.wallet.address, + b4a.toString(amount, 'hex'), + b4a.toString(txValidity, 'hex'), + 'json' + ); return { - raw: await new CompleteStateMessageOperations(admin.wallet, config) - .assembleCompleteTransferOperationMessage( + raw: safeEncodeApplyOperation( + await applyStateMessageFactory(admin.wallet, config) + .buildCompleteTransferOperationMessage( tx.address, b4a.from(tx.tro.tx, 'hex'), b4a.from(tx.tro.txv, 'hex'), @@ -33,7 +40,8 @@ const buildTransfer = async (admin, from, to, amount) => { tx.tro.to, b4a.from(tx.tro.am, 'hex'), b4a.from(tx.tro.is, 'hex'), - ), + ) + ), hash: tx.tro.tx } } diff --git a/tests/unit/messageOperations/assembleAddIndexerMessage.test.js b/tests/unit/messageOperations/assembleAddIndexerMessage.test.js deleted file mode 100644 index 4ad06f1e..00000000 --- a/tests/unit/messageOperations/assembleAddIndexerMessage.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import test from 'brittle'; -import CompleteStateMessageOperations from '../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; -import { OperationType } from '../../src/utils/protobuf/applyOperations.cjs'; -import { writingKeyNonAdmin, walletNonAdmin, initAll ,walletAdmin} from '../fixtures/assembleMessage.fixtures.js'; -import { messageOperationsBkoTest } from './commonsStateMessageOperationsTest.js'; -import { safeDecodeApplyOperation } from '../../src/utils/protobuf/operationHelpers.js'; -import { config } from '../../helpers/config.js'; - -const testName = 'assembleAddIndexerMessage'; -test(testName, async (t) => { - await initAll(); - const assembler = async (wallet, address) => { - return safeDecodeApplyOperation(await new CompleteStateMessageOperations(wallet, config).assembleAddIndexerMessage(address)); - } - - await messageOperationsBkoTest(t, testName, assembler, walletAdmin, writingKeyNonAdmin, OperationType.ADD_INDEXER, 2, walletNonAdmin.address); - -}); - - - diff --git a/tests/unit/messageOperations/assembleAddWriterMessage.test.js b/tests/unit/messageOperations/assembleAddWriterMessage.test.js deleted file mode 100644 index 4c6a286f..00000000 --- a/tests/unit/messageOperations/assembleAddWriterMessage.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import test from 'brittle'; -import CompleteStateMessageOperations from '../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; -import {OperationType} from '../../src/utils/protobuf/applyOperations.cjs'; -import {initAll, walletNonAdmin, writingKeyNonAdmin} from '../fixtures/assembleMessage.fixtures.js'; -import {messageOperationsEkoTest} from './commonsStateMessageOperationsTest.js'; -import {safeDecodeApplyOperation} from '../../src/utils/protobuf/operationHelpers.js'; -import { config } from '../../helpers/config.js' - -const testName = 'assembleAddWriterMessage'; -test(testName, async (t) => { - await initAll(); - const assembler = async (wallet, writingKey) => { - return safeDecodeApplyOperation(await new CompleteStateMessageOperations(wallet, config).assembleAddWriterMessage(writingKey)); - } - - await messageOperationsEkoTest(t, testName, assembler, walletNonAdmin, writingKeyNonAdmin, OperationType.ADD_WRITER, 3, walletNonAdmin.address); -}); \ No newline at end of file diff --git a/tests/unit/messageOperations/assembleAdminMessage.test.js b/tests/unit/messageOperations/assembleAdminMessage.test.js deleted file mode 100644 index 1ded896c..00000000 --- a/tests/unit/messageOperations/assembleAdminMessage.test.js +++ /dev/null @@ -1,68 +0,0 @@ -import test from 'brittle'; -import b4a from 'b4a'; -import CompleteStateMessageOperations from '../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; -import {default as fixtures} from '../fixtures/assembleMessage.fixtures.js'; -import {OperationType} from "../../src/utils/constants.js"; -import {bufferToAddress} from "../../src/core/state/utils/address.js"; -import {safeDecodeApplyOperation} from '../../src/utils/protobuf/operationHelpers.js'; -import {isAddressValid} from "../../src/core/state/utils/address.js"; -import {errorMessageIncludes} from "../utils/regexHelper.js"; -import { config } from '../../helpers/config.js'; - -test('assembleAdminMessage', async (t) => { - await fixtures.initAll(); - - const walletAdmin = fixtures.walletAdmin; - const writingKeyAdmin = fixtures.writingKeyAdmin; - const writingKeyNonAdmin = fixtures.writingKeyNonAdmin; - - - t.test('assembleAdminMessage - setup admin', async (k) => { - const msg = safeDecodeApplyOperation(await new CompleteStateMessageOperations(walletAdmin, config).assembleAddAdminMessage(writingKeyAdmin)); - - k.ok(msg, 'Message should be created'); - k.is(Object.keys(msg).length, 3, 'Message should have 3 keys'); - k.is(Object.keys(msg.eko).length, 3, 'Message value have 3 keys'); - k.is(msg.type, OperationType.ADD_ADMIN, 'Message type should be ADD_ADMIN'); - k.is(bufferToAddress(msg.address, config.addressPrefix), walletAdmin.address, 'Message address should be the public key of the wallet'); - - k.ok(isAddressValid(msg.address, config.addressPrefix), 'Message address should be a valid address'); - - k.ok(b4a.equals(msg.eko.wk, writingKeyAdmin), 'Message wk should be the writing key'); - k.is(msg.eko.nonce.length, 32, 'Message nonce should be 32 bytes long'); - k.ok(b4a.isBuffer(msg.eko.nonce), 'Message nonce should be a buffer'); - k.is(msg.eko.sig.length, 64, 'Message signature should be 64 bytes long') - k.ok(b4a.isBuffer(msg.eko.sig), 'Message signature should be a buffer'); - }); - - t.test('assembleAdminMessage - admin recovery message', async (k) => { - const msg = safeDecodeApplyOperation(await new CompleteStateMessageOperations(walletAdmin, config).assembleAddAdminMessage(writingKeyNonAdmin)); - - k.ok(msg, 'Message should be created'); - k.is(Object.keys(msg).length, 3, 'Message should have 3 keys'); - k.is(Object.keys(msg.eko).length, 3, 'Message value have 3 keys'); - k.is(msg.type, OperationType.ADD_ADMIN, 'Message type should be ADD_ADMIN'); - - k.is(bufferToAddress(msg.address, config.addressPrefix), walletAdmin.address, 'Message address should be address of the wallet'); - k.ok(isAddressValid(msg.address, config.addressPrefix), 'Message address should be a valid address'); - - k.ok(b4a.equals(msg.eko.wk, writingKeyNonAdmin), 'Message wk should be the writing key'); - k.is(msg.eko.sig.length, 64, 'Message signature should be 64 bytes long') - k.ok(b4a.isBuffer(msg.eko.sig), 'Message signature should be a buffer'); - - }); - - t.test('assembleAdminMessage - writer key is null', async (k) => { - await k.exception( - async () => await new CompleteStateMessageOperations(walletAdmin, config).assembleAddAdminMessage(null), - errorMessageIncludes('Writer key must be a 32 length buffer') - ); - }); - - t.test("assembleAdminMessage - admin wallet is null", async (k) => { - await k.exception( - async () => await new CompleteStateMessageOperations(null, config).assembleAddAdminMessage(writingKeyAdmin), - errorMessageIncludes('Wallet must be a valid wallet object') - ); - }); -}); \ No newline at end of file diff --git a/tests/unit/messageOperations/assembleBanWriterMessage.test.js b/tests/unit/messageOperations/assembleBanWriterMessage.test.js deleted file mode 100644 index bda168b3..00000000 --- a/tests/unit/messageOperations/assembleBanWriterMessage.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import test from 'brittle'; -import CompleteStateMessageOperations from '../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; -import { OperationType } from '../../src/utils/protobuf/applyOperations.cjs'; -import { writingKeyNonAdmin, walletNonAdmin, initAll, walletAdmin } from '../fixtures/assembleMessage.fixtures.js'; -import { messageOperationsBkoTest } from './commonsStateMessageOperationsTest.js'; -import { safeDecodeApplyOperation } from '../../src/utils/protobuf/operationHelpers.js'; -import { config } from '../../helpers/config.js'; - -const testName = 'assembleBanWriterMessage'; -test(testName, async (t) => { - await initAll(); - const assembler = async (wallet,address) => { - return safeDecodeApplyOperation(await new CompleteStateMessageOperations(wallet, config).assembleBanWriterMessage(address)); - } - await messageOperationsBkoTest(t, testName, assembler, walletAdmin, writingKeyNonAdmin, OperationType.BAN_VALIDATOR, 2, walletNonAdmin.address); - -}); diff --git a/tests/unit/messageOperations/assemblePostTransaction.test.js b/tests/unit/messageOperations/assemblePostTransaction.test.js deleted file mode 100644 index d98584ca..00000000 --- a/tests/unit/messageOperations/assemblePostTransaction.test.js +++ /dev/null @@ -1,424 +0,0 @@ -import test from 'brittle'; -import CompleteStateMessageOperations from '../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; -import {default as fixtures} from '../fixtures/assembleMessage.fixtures.js'; -import {OperationType} from "../../src/utils/constants.js"; -import {bufferToAddress} from "../../src/core/state/utils/address.js"; -import b4a from 'b4a'; -import {safeDecodeApplyOperation} from '../../src/utils/protobuf/operationHelpers.js'; -import {isAddressValid} from "../../src/core/state/utils/address.js"; -import {errorMessageIncludes} from "../utils/regexHelper.js"; -import {randomBytes} from "../../helpers/setupApplyTests.js"; -import { config } from '../../helpers/config.js'; - -const msgTxoLength = 10; -const opType = OperationType.TX; -test('assemblePostTxMessage - ....', async (k) => { - await fixtures.initAll(); - const nonAdminWallet = fixtures.walletNonAdmin; - const peerWallet = fixtures.walletPeer; - const validatorAddress = nonAdminWallet.address; - const txHash = randomBytes(32); - const incomingAddress = peerWallet.address; - - const incomingWriterKey = randomBytes(32); - const incomingNonce = randomBytes(32); - const contentHash = randomBytes(32); - const incomingSignature = randomBytes(64); - const externalBootstrap = randomBytes(32); - const msbBootstrap = randomBytes(32); - - const decodedPostTx = safeDecodeApplyOperation(await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - )); - k.ok(decodedPostTx, 'Message should be created'); - k.is(Object.keys(decodedPostTx).length, 3, 'Message should have 3 keys'); - k.is(Object.keys(decodedPostTx.txo).length, msgTxoLength, `Message value should have ${msgTxoLength} keys`); - - k.is(decodedPostTx.type, opType, `Message type should be ${opType}`); - - k.ok(isAddressValid(decodedPostTx.address, config.addressPrefix), 'Message validator address should be a valid address'); - k.ok(isAddressValid(decodedPostTx.txo.ia, config.addressPrefix), 'Message incoming address should be a valid address'); - - k.ok(bufferToAddress(decodedPostTx.txo.ia, config.addressPrefix) === incomingAddress, 'Message incoming address should be the address of the peer wallet'); - k.ok(bufferToAddress(decodedPostTx.address, config.addressPrefix) === validatorAddress, 'Message validator address should be the address of the non-admin wallet'); - - k.ok(b4a.isBuffer(decodedPostTx.txo.tx), 'tx should be a buffer'); - k.is(decodedPostTx.txo.tx.length, 32, 'tx should be 32 bytes long'); - k.ok(b4a.isBuffer(decodedPostTx.txo.ia), 'ia should be a buffer'); - k.is(decodedPostTx.txo.ia.length, 63, 'ia should be 63 bytes long'); - k.ok(b4a.isBuffer(decodedPostTx.txo.iw), 'iw should be a buffer'); - k.is(decodedPostTx.txo.iw.length, 32, 'iw should be 32 bytes long'); - k.ok(b4a.isBuffer(decodedPostTx.txo.in), 'in should be a buffer'); - k.is(decodedPostTx.txo.in.length, 32, 'in should be 32 bytes long'); - k.ok(b4a.isBuffer(decodedPostTx.txo.ch), 'ch should be a buffer'); - k.is(decodedPostTx.txo.ch.length, 32, 'ch should be 32 bytes long'); - k.ok(b4a.isBuffer(decodedPostTx.txo.is), 'is should be a buffer'); - k.is(decodedPostTx.txo.is.length, 64, 'is should be 64 bytes long'); - k.ok(b4a.isBuffer(decodedPostTx.txo.bs), 'bs should be a buffer'); - k.is(decodedPostTx.txo.bs.length, 32, 'bs should be 32 bytes long'); - k.ok(b4a.isBuffer(decodedPostTx.txo.mbs), 'mbs should be a buffer'); - k.is(decodedPostTx.txo.mbs.length, 32, 'mbs should be 32 bytes long'); - k.ok(b4a.isBuffer(decodedPostTx.txo.vs), 'vs should be a buffer'); - k.is(decodedPostTx.txo.vs.length, 64, 'vs should be 64 bytes long'); - k.ok(b4a.isBuffer(decodedPostTx.txo.vn), 'vn should be a buffer'); - k.is(decodedPostTx.txo.vn.length, 32, 'vn should be 32 bytes long'); - - k.test(`assemblePostTxMessage - Invalid wallet instance - trac address is to short`, async (k) => { - const invalidWallet = { - address: 'trac1y6kkq48fgu3ur' - } - await k.exception( - async () => await new CompleteStateMessageOperations(invalidWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - k.test(`assemblePostTxMessage - Invalid wallet instance - invalid prefix`, async (k) => { - const invalidWallet = { - address: 'testnet1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljk' - } - await k.exception( - async () => await new CompleteStateMessageOperations(invalidWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - k.test(`assemblePostTxMessage - Invalid wallet instance - empty string`, async (k) => { - const invalidWallet = { - address: '' - } - await k.exception( - async () => await new CompleteStateMessageOperations(invalidWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - k.test(`assemblePostTxMessage - Invalid wallet instance - null Wallet `, async (k) => { - await k.exception( - async () => await new CompleteStateMessageOperations(null, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), - errorMessageIncludes('Wallet must be a valid wallet object') - ); - }); - - k.test(`assemblePostTxMessage - Invalid wallet instance - undefined Wallet`, async (k) => { - await k.exception( - async () => await new CompleteStateMessageOperations(undefined, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), - errorMessageIncludes('Wallet must be a valid wallet object') - ); - }); - - k.test(`assemblePostTxMessage - Address parameter (validator address) - 'ą' does not belongs to the TRAC bench alphabet`, async (k) => { - - const invalid = 'trac1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljką'; - - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - invalid, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), - errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - k.test(`assemblePostTxMessage - Address parameter (validator address) - trac address is to short`, async (k) => { - const invalid = 'trac1y6kkq48fgu3ur'; - - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - invalid, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), - errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - k.test(`assemblePostTxMessage- Address parameter (validator address) - invalid prefix`, async (k) => { - const invalid = 'testnet1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljk'; - - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - invalid, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - - k.test(`assemblePostTxMessage - Address parameter (validator address) - empty string`, async (k) => { - const invalid = ''; - - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - invalid, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - k.test(`assemblePostTxMessage - Address parameter (validator address) - Null`, async (k) => { - - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - null, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - k.test(`assemblePostTxMessage - Address parameter (validator address) - undefined`, async (k) => { - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - undefined, - txHash, - incomingAddress, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - k.test(`assemblePostTxMessage - Address parameter (incoming address) - 'ą' does not belongs to the TRAC bench alphabet`, async (k) => { - const invalid = 'trac1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljką'; - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, // correct validator address - txHash, - invalid, // invalid incoming address - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), - errorMessageIncludes('Incoming address must be a 63 length string') - ); - }); - - k.test(`assemblePostTxMessage - Address parameter (incoming address) - trac address is to short`, async (k) => { - const invalid = 'trac1y6kkq48fgu3ur'; - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - invalid, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), - errorMessageIncludes('Incoming address must be a 63 length string') - ); - }); - - k.test(`assemblePostTxMessage- Address parameter (incoming address) - invalid prefix`, async (k) => { - const invalid = 'testnet1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljk'; - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - invalid, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), errorMessageIncludes('Incoming address must be a 63 length string') - ); - }); - - k.test(`assemblePostTxMessage - Address parameter (incoming address) - empty string`, async (k) => { - const invalid = ''; - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - invalid, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), errorMessageIncludes('Incoming address must be a 63 length string') - ); - }); - - k.test(`assemblePostTxMessage - Address parameter (incoming address) - Null`, async (k) => { - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - null, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), errorMessageIncludes('Incoming address must be a 63 length string') - ); - }); - - k.test(`assemblePostTxMessage - Address parameter (incoming address) - undefined`, async (k) => { - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - txHash, - undefined, - incomingWriterKey, - incomingNonce, - contentHash, - incomingSignature, - externalBootstrap, - msbBootstrap - ), errorMessageIncludes('Incoming address must be a 63 length string') - ); - }); - - const invalidBufferCases = [ - { name: 'empty buffer', value: b4a.alloc(0) }, - { name: 'null', value: null }, - { name: 'undefined', value: undefined }, - ]; - const bufferParams = [ - { key: 'msbBootstrap', error: 'MSB bootstrap must be a 32-byte buffer.' }, - { key: 'externalBootstrap', error: 'Bootstrap key must be a 32-byte buffer.' }, - { key: 'incomingSignature', error: 'Incoming signature must be a 64-byte buffer.' }, - { key: 'contentHash', error: 'Content hash must be a 32-byte buffer.' }, - { key: 'incomingNonce', error: 'Incoming nonce must be a 32-byte buffer.' }, - { key: 'incomingWriterKey', error: 'Incoming writer key must be a 32-byte buffer.' }, - { key: 'txHash', error: 'Transaction hash must be a 32-byte buffer.' }, - ]; - for (const param of bufferParams) { - for (const invalid of invalidBufferCases) { - k.test(`assemblePostTxMessage - ${param.key} - ${invalid.name}`, async (k) => { - const args = { - msbBootstrap, - externalBootstrap, - incomingSignature, - contentHash, - incomingNonce, - incomingWriterKey, - txHash - }; - args[param.key] = invalid.value; - await k.exception( - async () => await new CompleteStateMessageOperations(nonAdminWallet, config).assembleCompleteTransactionOperationMessage( - validatorAddress, - args.txHash, - incomingAddress, - args.incomingWriterKey, - args.incomingNonce, - args.contentHash, - args.incomingSignature, - args.externalBootstrap, - args.msbBootstrap - ), - errorMessageIncludes(param.error) - ); - }); - } - } - -}) diff --git a/tests/unit/messageOperations/assembleRemoveIndexerMessage.test.js b/tests/unit/messageOperations/assembleRemoveIndexerMessage.test.js deleted file mode 100644 index e3921bde..00000000 --- a/tests/unit/messageOperations/assembleRemoveIndexerMessage.test.js +++ /dev/null @@ -1,19 +0,0 @@ -import test from 'brittle'; -import CompleteStateMessageOperations from '../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; -import { OperationType } from '../../src/utils/protobuf/applyOperations.cjs'; -import { writingKeyNonAdmin, walletNonAdmin, initAll ,walletAdmin} from '../fixtures/assembleMessage.fixtures.js'; -import { messageOperationsBkoTest } from './commonsStateMessageOperationsTest.js'; -import { safeDecodeApplyOperation } from '../../src/utils/protobuf/operationHelpers.js'; -import { config } from '../../helpers/config.js'; - -const testName = 'assembleRemoveIndexerMessage'; -test(testName, async (t) => { - await initAll(); - const assembler = async (wallet,address) => { - return safeDecodeApplyOperation(await new CompleteStateMessageOperations(wallet, config).assembleRemoveIndexerMessage(address)); - } - - await messageOperationsBkoTest(t, testName, assembler, walletAdmin, writingKeyNonAdmin, OperationType.REMOVE_INDEXER, 2, walletNonAdmin.address); -}); - - diff --git a/tests/unit/messageOperations/assembleRemoveWriterMessage.test.js b/tests/unit/messageOperations/assembleRemoveWriterMessage.test.js deleted file mode 100644 index 17699ac2..00000000 --- a/tests/unit/messageOperations/assembleRemoveWriterMessage.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import test from 'brittle'; -import CompleteStateMessageOperations from '../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; -import {OperationType} from '../../src/utils/protobuf/applyOperations.cjs'; -import {initAll, walletNonAdmin, writingKeyNonAdmin} from '../fixtures/assembleMessage.fixtures.js'; -import {messageOperationsEkoTest} from './commonsStateMessageOperationsTest.js'; -import {safeDecodeApplyOperation} from '../../src/utils/protobuf/operationHelpers.js'; -import { config } from '../../helpers/config.js'; - -const testName = 'assembleRemoveWriterMessage'; -test(testName, async (t) => { - await initAll(); - const assembler = async (wallet, writingKey) => { - return safeDecodeApplyOperation(await new CompleteStateMessageOperations(wallet, config).assembleRemoveWriterMessage(writingKey)); - } - await messageOperationsEkoTest(t, testName, assembler, walletNonAdmin, writingKeyNonAdmin, OperationType.REMOVE_WRITER, 3, walletNonAdmin.address); -}); - diff --git a/tests/unit/messageOperations/assembleWhitelistMessages.test.js b/tests/unit/messageOperations/assembleWhitelistMessages.test.js deleted file mode 100644 index 0920e835..00000000 --- a/tests/unit/messageOperations/assembleWhitelistMessages.test.js +++ /dev/null @@ -1,59 +0,0 @@ -import test from 'brittle'; -import { OperationType } from '../../src/utils/protobuf/applyOperations.cjs'; -import { default as fixtures } from '../fixtures/assembleMessage.fixtures.js'; -import { safeDecodeApplyOperation } from '../../src/utils/protobuf/operationHelpers.js'; -import b4a from 'b4a'; -import fileUtils from "../../src/utils/fileUtils.js"; -import CompleteStateMessageOperations from "../../src/messages/completeStateMessages/CompleteStateMessageOperations.js"; -import {bufferToAddress} from "../../src/core/state/utils/address.js"; -import {errorMessageIncludes} from "../utils/regexHelper.js"; -import { config } from '../../helpers/config.js' - -// MOCK SETUP -const whitelistAddresses = [ - 'trac1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljk', -]; -const originalReadAddressesFromWhitelistFile = fileUtils.readAddressesFromWhitelistFile; - -test('assembleWhitelistMessages', async (t) => { - fileUtils.readAddressesFromWhitelistFile = async () => whitelistAddresses; - - await fixtures.initAll(); - const walletAdmin = fixtures.walletAdmin; - - - t.test('assembleWhitelistMessages - Happy Path', async (k) => { - const mapMsg = await new CompleteStateMessageOperations(walletAdmin, config).assembleAppendWhitelistMessages(); - const msg = mapMsg.get(whitelistAddresses[0]) - k.ok(msg, 'Message should be created'); - k.ok(msg.length > 0, 'Message should be an array with at least one element'); - const decodedMsg = safeDecodeApplyOperation(msg); - k.is(Object.keys(decodedMsg).length, 3, 'Message should have 3 keys'); - k.is(Object.keys(decodedMsg.bko).length, 2, 'Message value should have 2 keys'); - k.is(decodedMsg.type, OperationType.APPEND_WHITELIST, 'Message type should be APPEND_WHITELIST'); - - k.is(bufferToAddress(decodedMsg.address, config.addressPrefix) , whitelistAddresses[0], 'Message address should be the address in the file'); - k.is(decodedMsg.bko.nonce.length, 32, 'Message nonce should be 32 bytes long'); - k.ok(b4a.isBuffer(decodedMsg.bko.nonce), 'Message nonce should be a buffer'); - k.is(decodedMsg.bko.sig.length, 64, 'Message signature should be 64 bytes long'); - k.ok(b4a.isBuffer(decodedMsg.bko.sig), 'Message signature should be a buffer'); - }); - - t.test('assembleWhitelistMessages - Should return null when wallet is invalid', async (k) => { - await k.exception( - async () => await new CompleteStateMessageOperations(null, config).assembleAppendWhitelistMessages(), - errorMessageIncludes('Wallet must be a valid wallet object') - ); - }); - - - t.test('assembleWhitelistMessages - Empty object', async (k) => { - await k.exception( - async () => await new CompleteStateMessageOperations({}, config).assembleAppendWhitelistMessages(), - errorMessageIncludes('Wallet should have a valid TRAC address.') - ); - - }); - - fileUtils.readAddressesFromWhitelistFile = originalReadAddressesFromWhitelistFile; -}); diff --git a/tests/unit/messageOperations/commonsStateMessageOperationsTest.js b/tests/unit/messageOperations/commonsStateMessageOperationsTest.js deleted file mode 100644 index 64c77961..00000000 --- a/tests/unit/messageOperations/commonsStateMessageOperationsTest.js +++ /dev/null @@ -1,278 +0,0 @@ -import b4a from 'b4a'; -import {OperationType} from "../../src/utils/constants.js"; -import {bufferToAddress, isAddressValid} from "../../src/core/state/utils/address.js"; -import {errorMessageIncludes} from "../utils/regexHelper.js" -import { config } from '../../helpers/config.js' - -export async function messageOperationsEkoTest(t, fnName, assembler, wallet, writingKey, opType, msgValueLength, expectedMessageAddress) { - console.log('address:', expectedMessageAddress) - t.test(`${fnName} - Happy Path`, async (k) => { - const msg = await assembler(wallet, writingKey); - k.ok(msg, 'Message should be created'); - k.is(Object.keys(msg).length, 3, 'Message should have 3 keys'); - k.is(Object.keys(msg.eko).length, msgValueLength, `Message value should have ${msgValueLength} keys`); - - k.is(msg.type, opType, `Message type should be ${opType}`); - - if (msg.type === OperationType.ADD_WRITER || msg.type === OperationType.REMOVE_WRITER || msg.type === OperationType.ADD_ADMIN) { - k.ok(b4a.equals(msg.eko.wk, writingKey), 'Message wk should be the writing key'); - } - - k.ok(bufferToAddress(msg.address, config.addressPrefix) === expectedMessageAddress, 'Message key should be the the expected one'); - k.ok(isAddressValid(msg.address, config.addressPrefix), 'Message address should be a valid address'); - - k.is(msg.eko.nonce.length, 32, 'Message nonce should be 32 bytes long'); - k.ok(b4a.isBuffer(msg.eko.nonce), 'Message nonce should be a buffer'); - k.is(msg.eko.sig.length, 64, 'Message signature should be 64 bytes long'); - k.ok(b4a.isBuffer(msg.eko.sig), 'Message signature should be a buffer'); - }); - - t.test(`${fnName} - Invalid wallet - 'ą' does not belongs to the TRAC bench alphabet`, async (k) => { - const wallet = { - address: 'trac1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljką' - } - await k.exception( - async () => await assembler(wallet, writingKey), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - t.test(`${fnName} - Invalid wallet instance - trac address is to short`, async (k) => { - const wallet = { - address: 'trac1y6kkq48fgu3ur' - } - await k.exception( - async () => await assembler(wallet, writingKey), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - t.test(`${fnName} - Invalid wallet instance - invalid prefix`, async (k) => { - const wallet = { - address: 'testnet1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljk' - } - await k.exception( - async () => await assembler(wallet, writingKey), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - t.test(`${fnName} - Invalid wallet instance - empty string`, async (k) => { - const wallet = { - address: '' - } - await k.exception( - async () => await assembler(wallet, writingKey), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - t.test(`${fnName} - Invalid wallet instance - null Wallet `, async (k) => { - await k.exception( - async () => await assembler(null, writingKey), - errorMessageIncludes('Wallet must be a valid wallet object') - ); - }); - - t.test(`${fnName} - Invalid wallet instance - undefined Wallet`, async (k) => { - await k.exception( - async () => await assembler(undefined, writingKey), - errorMessageIncludes('Wallet must be a valid wallet object') - ); - }); - // - // //TODO: fix - works on node, but not on bare. - // - // - // t.test(`${fnName} - Invalid writing key - not hex`, async (k) => { - // try { - // const invalidHexKey = b4a.from("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeg", 'hex'); - // await k.exception( - // async () => await assembler(wallet, invalidHexKey), - // errorMessageIncludes('Writer key must be a 32 length buffer') - // ); - // } catch (error) { - // k.pass('Invalid hex string was rejected'); - // } - // }); - - - t.test(`${fnName} - Invalid writing key - invalid length`, async (k) => { - await k.exception( - async () => await assembler( - wallet, - b4a.from("1234567890a", 'hex') - ), - errorMessageIncludes('Writer key must be a 32 length buffer') - ); - }); - - - t.test(`${fnName} - Invalid writing key - empty buffer`, async (k) => { - await k.exception( - async () => await assembler( - wallet, - b4a.alloc(0) - ), - errorMessageIncludes('Writer key must be a 32 length buffer') - ); - }); - - t.test(`${fnName} - Null writing key`, async (k) => { - await k.exception( - async () => await assembler( - wallet, - null - ), - errorMessageIncludes('Writer key must be a 32 length buffer') - ); - }); - - - t.test(`${fnName} - undefined writing key`, async (k) => { - await k.exception( - async () => await assembler( - wallet, - undefined - ), - errorMessageIncludes('Writer key must be a 32 length buffer') - ); - }); - -} - -export async function messageOperationsBkoTest(t, fnName, assembler, wallet, writingKey, opType, msgValueLength, expectedMessageAddress) { - - t.test(`${fnName} - Happy Path`, async (k) => { - const msg = await assembler(wallet, expectedMessageAddress); - - k.ok(msg, 'Message should be created'); - k.is(Object.keys(msg).length, 3, 'Message should have 3 keys'); - k.is(Object.keys(msg.bko).length, msgValueLength, `Message value should have ${msgValueLength} keys`); - - - k.is(msg.type, opType, `Message type should be ${opType}`); - - k.ok(bufferToAddress(msg.address, config.addressPrefix) === expectedMessageAddress, 'Message address should be the the expected one'); - k.is(msg.bko.nonce.length, 32, 'Message nonce should be 32 bytes long'); - k.ok(b4a.isBuffer(msg.bko.nonce), 'Message nonce should be a buffer'); - k.is(msg.bko.sig.length, 64, 'Message signature should be 64 bytes long'); - k.ok(b4a.isBuffer(msg.bko.sig), 'Message signature should be a buffer'); - }); - - t.test(`${fnName} - Invalid wallet instance - 'ą' does not belongs to the TRAC bench alphabet`, async (k) => { - const invalidWallet = { - address: 'trac1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljką' - } - - await k.exception( - async () => await assembler(invalidWallet, expectedMessageAddress), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - t.test(`${fnName} - Invalid wallet instance - trac address is to short`, async (k) => { - const wallet = { - address: 'trac1y6kkq48fgu3ur' - } - await k.exception( - async () => await assembler(wallet, expectedMessageAddress), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - t.test(`${fnName} - Invalid wallet instance - invalid prefix`, async (k) => { - const wallet = { - address: 'testnet1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljk' - } - await k.exception( - async () => await assembler(wallet, expectedMessageAddress), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - - t.test(`${fnName} - Invalid wallet instance - empty string`, async (k) => { - const wallet = { - address: '' - } - await k.exception( - async () => await assembler(wallet, expectedMessageAddress), - errorMessageIncludes('Wallet should have a valid TRAC address') - ); - }); - - t.test(`${fnName} - Invalid wallet instance - Null Wallet`, async (k) => { - await k.exception( - async () => await assembler(null, expectedMessageAddress), - errorMessageIncludes('Wallet must be a valid wallet object') - ); - }); - - t.test(`${fnName} - Invalid wallet instance - undefined Wallet`, async (k) => { - await k.exception( - async () => await assembler(undefined, expectedMessageAddress), - errorMessageIncludes('Wallet must be a valid wallet object') - ); - }); - - t.test(`${fnName} - Address parameter - 'ą' does not belongs to the TRAC bench alphabet`, async (k) => { - - const invalid = 'trac1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljką'; - - await k.exception( - async () => await assembler(wallet, invalid), - errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - t.test(`${fnName} - Address parameter - trac address is to short`, async (k) => { - const invalid = 'trac1y6kkq48fgu3ur'; - - await k.exception( - async () => await assembler(wallet, invalid), - errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - t.test(`${fnName} - Address parameter - invalid prefix`, async (k) => { - const invalid = 'testnet1y6kkq48fgu3urrhg0gm7h8zdyxl3gnaazd2u7568lfl5zxqs285q6kuljk'; - - await k.exception( - async () => await assembler(wallet, invalid), - errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - - t.test(`${fnName} - Address parameter - empty string`, async (k) => { - const invalid = ''; - - await k.exception( - async () => await assembler(wallet, invalid), - errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - t.test(`${fnName} - Address parameter - Null`, async (k) => { - - await k.exception( - async () => await assembler(wallet, null), - errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - t.test(`${fnName} - Address parameter - undefined`, async (k) => { - await k.exception( - async () => await assembler(wallet, undefined), - errorMessageIncludes('Address field must be a valid TRAC bech32m address with length 63') - ); - }); - - t.test(`${fnName} - Address parameter - address is the same as wallet address`, async (k) => { - await k.exception( - async () => await assembler(wallet, wallet.address), - errorMessageIncludes('Address must not be the same as the wallet address for basic operations') - ); - }); -} diff --git a/tests/unit/messageOperations/stateMessageOperations.test.js b/tests/unit/messageOperations/stateMessageOperations.test.js deleted file mode 100644 index 2dd8b94c..00000000 --- a/tests/unit/messageOperations/stateMessageOperations.test.js +++ /dev/null @@ -1,19 +0,0 @@ -import { default as test } from 'brittle'; - -async function runMsgUtilsTests() { - test.pause(); - await import('./assembleAdminMessage.test.js'); - await import('./assembleAddWriterMessage.test.js'); - await import('./assembleRemoveWriterMessage.test.js'); - await import('./assembleAddIndexerMessage.test.js'); - await import('./assembleRemoveIndexerMessage.test.js'); - await import('./assembleBanWriterMessage.test.js'); - await import('./assembleWhitelistMessages.test.js'); - await import('./assembleWhitelistMessages.test.js'); - await import('./assemblePostTransaction.test.js'); - - // TODO: Implement mocked tests for MessageOperations.verifyEventMessage - test.resume(); -} - -await runMsgUtilsTests(); \ No newline at end of file diff --git a/tests/unit/messages/messages.test.js b/tests/unit/messages/messages.test.js new file mode 100644 index 00000000..95cc6dfe --- /dev/null +++ b/tests/unit/messages/messages.test.js @@ -0,0 +1,12 @@ +import { default as test } from 'brittle'; + +async function runMsgUtilsTests() { + test.pause(); + await import('./network/NetworkMessageBuilder.test.js'); + await import('./network/NetworkMessageDirector.test.js'); + await import('./state/applyStateMessageBuilder.complete.test.js'); + await import('./state/applyStateMessageBuilder.partial.test.js'); + test.resume(); +} + +await runMsgUtilsTests(); \ No newline at end of file diff --git a/tests/unit/messages/network/NetworkMessageBuilder.test.js b/tests/unit/messages/network/NetworkMessageBuilder.test.js new file mode 100644 index 00000000..108812e9 --- /dev/null +++ b/tests/unit/messages/network/NetworkMessageBuilder.test.js @@ -0,0 +1,276 @@ +import { test } from 'brittle'; +import b4a from 'b4a'; +import PeerWallet from 'trac-wallet'; +import { TRAC_NETWORK_MSB_MAINNET_PREFIX } from 'trac-wallet/constants.js'; + +import NetworkWalletFactory from '../../../../src/core/network/identity/NetworkWalletFactory.js'; +import NetworkMessageBuilder from '../../../../src/messages/network/v1/NetworkMessageBuilder.js'; +import { + NetworkOperationType, + ResultCode as NetworkResultCode +} from '../../../../src/utils/constants.js'; +import { decodeV1networkOperation, encodeV1networkOperation } from '../../../../src/utils/protobuf/operationHelpers.js'; +import { errorMessageIncludes } from '../../../helpers/regexHelper.js'; +import { + createMessage, + encodeCapabilities, + safeWriteUInt32BE, + sessionIdToBuffer, + timestampToBuffer +} from '../../../../src/utils/buffer.js'; +import { addressToBuffer } from '../../../../src/core/state/utils/address.js'; +import { config } from '../../../helpers/config.js'; +import { testKeyPair1 } from '../../../fixtures/apply.fixtures.js'; + +function createWallet() { + const keyPair = { + publicKey: b4a.from(testKeyPair1.publicKey, 'hex'), + secretKey: b4a.from(testKeyPair1.secretKey, 'hex') + }; + return NetworkWalletFactory.provide({ + enableWallet: false, + keyPair, + networkPrefix: TRAC_NETWORK_MSB_MAINNET_PREFIX + }); +} + +function uniqueResultCodes() { + return [...new Set(Object.values(NetworkResultCode))].sort((a, b) => a - b); +} + +test('NetworkMessageBuilder builds validator connection request and verifies signature', async t => { + const wallet = createWallet(); + const builder = new NetworkMessageBuilder(wallet, config); + + const sessionId = 1; + const caps = ['cap:b', 'cap:a']; + + await builder + .setType(NetworkOperationType.VALIDATOR_CONNECTION_REQUEST) + .setSessionId(sessionId) + .setTimestamp() + .setIssuerAddress(wallet.address) + .setCapabilities(caps) + .buildPayload(); + + const payload = builder.getResult(); + t.is(payload.type, NetworkOperationType.VALIDATOR_CONNECTION_REQUEST); + t.is(payload.session_id, sessionId); + t.alike(payload.capabilities, caps); + t.ok(b4a.isBuffer(payload.validator_connection_request.nonce)); + t.ok(b4a.isBuffer(payload.validator_connection_request.signature)); + + const message = createMessage( + payload.type, + sessionIdToBuffer(sessionId), + timestampToBuffer(payload.timestamp), + addressToBuffer(wallet.address, config.addressPrefix), + payload.validator_connection_request.nonce, + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(message); + t.ok(wallet.verify(payload.validator_connection_request.signature, hash, wallet.publicKey)); + + const roundTrip = decodeV1networkOperation(encodeV1networkOperation(payload)); + t.is(roundTrip.type, NetworkOperationType.VALIDATOR_CONNECTION_REQUEST); +}); + +test('NetworkMessageBuilder iterates validator connection response ResultCode values', async t => { + const wallet = createWallet(); + const builder = new NetworkMessageBuilder(wallet, config); + + const otherAddress = 'trac1xm76l9qaujh7vqktk8302mw9sfrxau3l45w62hqfl4kasswt6yts0autkh'; + const caps = ['cap:b', 'cap:a']; + + for (const code of uniqueResultCodes()) { + await builder + .setType(NetworkOperationType.VALIDATOR_CONNECTION_RESPONSE) + .setSessionId(1) + .setTimestamp() + .setIssuerAddress(otherAddress) + .setCapabilities(caps) + .setResultCode(code) + .buildPayload(); + + const payload = builder.getResult(); + t.is(payload.type, NetworkOperationType.VALIDATOR_CONNECTION_RESPONSE); + t.is(payload.validator_connection_response.result, code); + + const msg = createMessage( + payload.type, + sessionIdToBuffer(payload.session_id), + timestampToBuffer(payload.timestamp), + addressToBuffer(otherAddress, config.addressPrefix), + payload.validator_connection_response.nonce, + safeWriteUInt32BE(code, 0), + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(msg); + t.ok(wallet.verify(payload.validator_connection_response.signature, hash, wallet.publicKey)); + + const decoded = decodeV1networkOperation(encodeV1networkOperation(payload)); + t.is(decoded.validator_connection_response.result, code); + } +}); + +test('NetworkMessageBuilder iterates liveness response ResultCode values', async t => { + const wallet = createWallet(); + const builder = new NetworkMessageBuilder(wallet, config); + const sessionId = 1; + const caps = ['cap:b', 'cap:a']; + const data = b4a.from('ping', 'utf8'); + + for (const code of uniqueResultCodes()) { + await builder + .setType(NetworkOperationType.LIVENESS_RESPONSE) + .setSessionId(sessionId) + .setTimestamp() + .setData(data) + .setCapabilities(caps) + .setResultCode(code) + .buildPayload(); + + const payload = builder.getResult(); + t.is(payload.type, NetworkOperationType.LIVENESS_RESPONSE); + t.is(payload.liveness_response.result, code); + + const msg = createMessage( + payload.type, + sessionIdToBuffer(payload.session_id), + timestampToBuffer(payload.timestamp), + payload.liveness_response.nonce, + safeWriteUInt32BE(code, 0), + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(msg); + t.ok(wallet.verify(payload.liveness_response.signature, hash, wallet.publicKey)); + + const decoded = decodeV1networkOperation(encodeV1networkOperation(payload)); + t.is(decoded.liveness_response.result, code); + } +}); + +test('NetworkMessageBuilder builds liveness request and verifies signature (data not signed)', async t => { + const wallet = createWallet(); + const builder = new NetworkMessageBuilder(wallet, config); + + const sessionId = 1; + const caps = ['cap:b', 'cap:a']; + const data = b4a.from('ping', 'utf8'); + + await builder + .setType(NetworkOperationType.LIVENESS_REQUEST) + .setSessionId(sessionId) + .setTimestamp() + .setData(data) + .setCapabilities(caps) + .buildPayload(); + + const payload = builder.getResult(); + t.is(payload.type, NetworkOperationType.LIVENESS_REQUEST); + t.ok(b4a.isBuffer(payload.liveness_request.nonce)); + t.ok(b4a.isBuffer(payload.liveness_request.signature)); + + const msg = createMessage( + payload.type, + sessionIdToBuffer(payload.session_id), + timestampToBuffer(payload.timestamp), + payload.liveness_request.nonce, + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(msg); + t.ok(wallet.verify(payload.liveness_request.signature, hash, wallet.publicKey)); +}); + +test('NetworkMessageBuilder iterates broadcast transaction response ResultCode values', async t => { + const wallet = createWallet(); + const builder = new NetworkMessageBuilder(wallet, config); + const sessionId = 1; + const caps = ['cap:b', 'cap:a']; + + for (const code of uniqueResultCodes()) { + await builder + .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE) + .setSessionId(sessionId) + .setTimestamp() + .setCapabilities(caps) + .setResultCode(code) + .buildPayload(); + + const payload = builder.getResult(); + t.is(payload.type, NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE); + t.is(payload.broadcast_transaction_response.result, code); + + const msg = createMessage( + payload.type, + sessionIdToBuffer(payload.session_id), + timestampToBuffer(payload.timestamp), + payload.broadcast_transaction_response.nonce, + safeWriteUInt32BE(code, 0), + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(msg); + t.ok(wallet.verify(payload.broadcast_transaction_response.signature, hash, wallet.publicKey)); + + const decoded = decodeV1networkOperation(encodeV1networkOperation(payload)); + t.is(decoded.broadcast_transaction_response.result, code); + } +}); + +test('NetworkMessageBuilder builds broadcast transaction request and verifies signature', async t => { + const wallet = createWallet(); + const builder = new NetworkMessageBuilder(wallet, config); + + const sessionId = 1; + const caps = ['cap:b', 'cap:a']; + const data = b4a.from('deadbeef', 'hex'); + + await builder + .setType(NetworkOperationType.BROADCAST_TRANSACTION_REQUEST) + .setSessionId(sessionId) + .setTimestamp() + .setData(data) + .setCapabilities(caps) + .buildPayload(); + + const payload = builder.getResult(); + t.is(payload.type, NetworkOperationType.BROADCAST_TRANSACTION_REQUEST); + t.alike(payload.broadcast_transaction_request.data, data); + + const msg = createMessage( + payload.type, + sessionIdToBuffer(payload.session_id), + timestampToBuffer(payload.timestamp), + data, + payload.broadcast_transaction_request.nonce, + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(msg); + t.ok(wallet.verify(payload.broadcast_transaction_request.signature, hash, wallet.publicKey)); +}); + +test('NetworkMessageBuilder validates required inputs', async t => { + const wallet = createWallet(); + const builder = new NetworkMessageBuilder(wallet, config); + + await t.exception( + () => builder.setType(undefined), + errorMessageIncludes('Invalid operation type') + ); + + await t.exception( + () => builder.setCapabilities('not-an-array'), + errorMessageIncludes('Capabilities must be a string array.') + ); + + await t.exception( + () => + builder + .setType(NetworkOperationType.BROADCAST_TRANSACTION_REQUEST) + .setSessionId(1) + .setTimestamp() + .setCapabilities([]) + .buildPayload(), + errorMessageIncludes('Data must be set before building broadcast transaction request') + ); +}); diff --git a/tests/unit/messages/network/NetworkMessageDirector.test.js b/tests/unit/messages/network/NetworkMessageDirector.test.js new file mode 100644 index 00000000..7e92e3ae --- /dev/null +++ b/tests/unit/messages/network/NetworkMessageDirector.test.js @@ -0,0 +1,203 @@ +import { test } from 'brittle'; +import b4a from 'b4a'; +import PeerWallet from 'trac-wallet'; +import { TRAC_NETWORK_MSB_MAINNET_PREFIX } from 'trac-wallet/constants.js'; + +import NetworkWalletFactory from '../../../../src/core/network/identity/NetworkWalletFactory.js'; +import NetworkMessageDirector from '../../../../src/messages/network/v1/NetworkMessageDirector.js'; +import NetworkMessageBuilder from '../../../../src/messages/network/v1/NetworkMessageBuilder.js'; +import { NetworkOperationType, ResultCode as NetworkResultCode } from '../../../../src/utils/constants.js'; +import { decodeV1networkOperation, encodeV1networkOperation } from '../../../../src/utils/protobuf/operationHelpers.js'; +import { + createMessage, + encodeCapabilities, + safeWriteUInt32BE, + sessionIdToBuffer, + timestampToBuffer +} from '../../../../src/utils/buffer.js'; +import { addressToBuffer } from '../../../../src/core/state/utils/address.js'; +import { config } from '../../../helpers/config.js'; +import { testKeyPair1 } from '../../../fixtures/apply.fixtures.js'; + +function createWallet() { + const keyPair = { + publicKey: b4a.from(testKeyPair1.publicKey, 'hex'), + secretKey: b4a.from(testKeyPair1.secretKey, 'hex') + }; + return NetworkWalletFactory.provide({ + enableWallet: false, + keyPair, + networkPrefix: TRAC_NETWORK_MSB_MAINNET_PREFIX + }); +} + +function uniqueResultCodes() { + return [...new Set(Object.values(NetworkResultCode))].sort((a, b) => a - b); +} + +test('NetworkMessageDirector builds validator connection request and verifies signature', async t => { + const wallet = createWallet(); + const director = new NetworkMessageDirector(new NetworkMessageBuilder(wallet, config)); + + const sessionId = 1; + const caps = ['cap:b', 'cap:a']; + + const payload = await director.buildValidatorConnectionRequest(sessionId, wallet.address, caps); + t.is(payload.type, NetworkOperationType.VALIDATOR_CONNECTION_REQUEST); + t.is(payload.session_id, sessionId); + t.alike(payload.capabilities, caps); + + const msg = createMessage( + payload.type, + sessionIdToBuffer(payload.session_id), + timestampToBuffer(payload.timestamp), + addressToBuffer(wallet.address, config.addressPrefix), + payload.validator_connection_request.nonce, + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(msg); + t.ok(wallet.verify(payload.validator_connection_request.signature, hash, wallet.publicKey)); +}); + +test('NetworkMessageDirector builds liveness request and verifies signature', async t => { + const wallet = createWallet(); + const director = new NetworkMessageDirector(new NetworkMessageBuilder(wallet, config)); + + const sessionId = 1; + const caps = ['cap:b', 'cap:a']; + const data = b4a.from('ping', 'utf8'); + + const payload = await director.buildLivenessRequest(sessionId, data, caps); + t.is(payload.type, NetworkOperationType.LIVENESS_REQUEST); + t.is(payload.session_id, sessionId); + t.alike(payload.capabilities, caps); + + const msg = createMessage( + payload.type, + sessionIdToBuffer(payload.session_id), + timestampToBuffer(payload.timestamp), + payload.liveness_request.nonce, + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(msg); + t.ok(wallet.verify(payload.liveness_request.signature, hash, wallet.publicKey)); +}); + +test('NetworkMessageDirector iterates liveness response ResultCode values', async t => { + const wallet = createWallet(); + const director = new NetworkMessageDirector(new NetworkMessageBuilder(wallet, config)); + + const sessionId = 1; + const caps = ['cap:b', 'cap:a']; + const data = b4a.from('ping', 'utf8'); + + for (const code of uniqueResultCodes()) { + const payload = await director.buildLivenessResponse(sessionId, data, caps, code); + t.is(payload.type, NetworkOperationType.LIVENESS_RESPONSE); + t.is(payload.liveness_response.result, code); + + const msg = createMessage( + payload.type, + sessionIdToBuffer(payload.session_id), + timestampToBuffer(payload.timestamp), + payload.liveness_response.nonce, + safeWriteUInt32BE(code, 0), + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(msg); + t.ok(wallet.verify(payload.liveness_response.signature, hash, wallet.publicKey)); + + const decoded = decodeV1networkOperation(encodeV1networkOperation(payload)); + t.is(decoded.liveness_response.result, code); + } +}); + +test('NetworkMessageDirector builds broadcast transaction request and verifies signature', async t => { + const wallet = createWallet(); + const director = new NetworkMessageDirector(new NetworkMessageBuilder(wallet, config)); + + const sessionId = 1; + const data = b4a.from('deadbeef', 'hex'); + const caps = ['cap:b', 'cap:a']; + + const payload = await director.buildBroadcastTransactionRequest(sessionId, data, caps); + t.is(payload.type, NetworkOperationType.BROADCAST_TRANSACTION_REQUEST); + t.is(payload.session_id, sessionId); + t.alike(payload.capabilities, caps); + t.alike(payload.broadcast_transaction_request.data, data); + + const message = createMessage( + payload.type, + sessionIdToBuffer(sessionId), + timestampToBuffer(payload.timestamp), + data, + payload.broadcast_transaction_request.nonce, + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(message); + t.ok(wallet.verify(payload.broadcast_transaction_request.signature, hash, wallet.publicKey)); + + const decoded = decodeV1networkOperation(encodeV1networkOperation(payload)); + t.is(decoded.type, NetworkOperationType.BROADCAST_TRANSACTION_REQUEST); +}); + +test('NetworkMessageDirector iterates broadcast transaction response ResultCode values', async t => { + const wallet = createWallet(); + const director = new NetworkMessageDirector(new NetworkMessageBuilder(wallet, config)); + + const sessionId = 1; + const caps = ['cap:b', 'cap:a']; + + for (const code of uniqueResultCodes()) { + const payload = await director.buildBroadcastTransactionResponse(sessionId, caps, code); + t.is(payload.type, NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE); + t.is(payload.broadcast_transaction_response.result, code); + + const msg = createMessage( + payload.type, + sessionIdToBuffer(payload.session_id), + timestampToBuffer(payload.timestamp), + payload.broadcast_transaction_response.nonce, + safeWriteUInt32BE(code, 0), + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(msg); + t.ok(wallet.verify(payload.broadcast_transaction_response.signature, hash, wallet.publicKey)); + + const decoded = decodeV1networkOperation(encodeV1networkOperation(payload)); + t.is(decoded.broadcast_transaction_response.result, code); + } +}); + +test('NetworkMessageDirector iterates validator connection response ResultCode values', async t => { + const wallet = createWallet(); + const director = new NetworkMessageDirector(new NetworkMessageBuilder(wallet, config)); + + const sessionId = 1; + const caps = ['cap:b', 'cap:a']; + const otherAddress = + 'trac1xm76l9qaujh7vqktk8302mw9sfrxau3l45w62hqfl4kasswt6yts0autkh'; + + for (const code of uniqueResultCodes()) { + const payload = await director.buildValidatorConnectionResponse( + sessionId, + otherAddress, + caps, + code + ); + t.is(payload.type, NetworkOperationType.VALIDATOR_CONNECTION_RESPONSE); + t.is(payload.validator_connection_response.result, code); + + const msg = createMessage( + payload.type, + sessionIdToBuffer(payload.session_id), + timestampToBuffer(payload.timestamp), + addressToBuffer(otherAddress, config.addressPrefix), + payload.validator_connection_response.nonce, + safeWriteUInt32BE(code, 0), + encodeCapabilities(caps) + ); + const hash = await PeerWallet.blake3(msg); + t.ok(wallet.verify(payload.validator_connection_response.signature, hash, wallet.publicKey)); + } +}); diff --git a/tests/unit/messages/state/applyStateMessageBuilder.complete.test.js b/tests/unit/messages/state/applyStateMessageBuilder.complete.test.js new file mode 100644 index 00000000..e3b4ead4 --- /dev/null +++ b/tests/unit/messages/state/applyStateMessageBuilder.complete.test.js @@ -0,0 +1,521 @@ +import { test } from 'brittle'; +import b4a from 'b4a'; +import PeerWallet from 'trac-wallet'; + +import ApplyStateMessageBuilder from '../../../../src/messages/state/ApplyStateMessageBuilder.js'; +import { OperationType } from '../../../../src/utils/constants.js'; +import { config } from '../../../helpers/config.js'; +import { testKeyPair1, testKeyPair2 } from '../../../fixtures/apply.fixtures.js'; +import { addressToBuffer } from '../../../../src/core/state/utils/address.js'; + +const hex = (value, bytes) => value.repeat(bytes); +const toBuf = value => b4a.from(value, 'hex'); + +async function createWallet(mnemonic) { + const wallet = new PeerWallet({ mnemonic, networkPrefix: config.addressPrefix }); + await wallet.ready; + return wallet; +} + +function expectBufferField(t, value, bytes, label) { + t.ok(b4a.isBuffer(value), `${label} type`); + t.is(value.length, bytes, `${label} length`); +} + +function expectAddressBuffer(t, value, label) { + expectBufferField(t, value, config.addressLength, label); +} + +function expectKeys(t, value, keys, label) { + t.alike(Object.keys(value).sort(), keys.slice().sort(), `${label} keys`); +} + +function expectPayloadKeys(t, payload, bodyKey) { + expectKeys(t, payload, ['type', 'address', bodyKey], 'payload'); +} + +test('ApplyStateMessageBuilder complete add admin (cao)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txValidity = toBuf(hex('11', 32)); + const writingKey = toBuf(hex('22', 32)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.ADD_ADMIN) + .setAddress(wallet.address) + .setWriterKey(writingKey) + .setTxValidity(txValidity) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.ADD_ADMIN); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'cao'); + expectKeys(t, payload.cao, ['tx', 'txv', 'iw', 'in', 'is'], 'cao'); + expectBufferField(t, payload.cao.tx, 32, 'cao.tx'); + expectBufferField(t, payload.cao.txv, 32, 'cao.txv'); + expectBufferField(t, payload.cao.iw, 32, 'cao.iw'); + t.ok(b4a.equals(payload.cao.txv, txValidity)); + t.ok(b4a.equals(payload.cao.iw, writingKey)); + expectBufferField(t, payload.cao.in, 32, 'cao.in'); + expectBufferField(t, payload.cao.is, 64, 'cao.is'); +}); + +test('ApplyStateMessageBuilder complete disable initialization (cao)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txValidity = toBuf(hex('33', 32)); + const writingKey = toBuf(hex('44', 32)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.DISABLE_INITIALIZATION) + .setAddress(wallet.address) + .setWriterKey(writingKey) + .setTxValidity(txValidity) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.DISABLE_INITIALIZATION); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'cao'); + expectKeys(t, payload.cao, ['tx', 'txv', 'iw', 'in', 'is'], 'cao'); + expectBufferField(t, payload.cao.tx, 32, 'cao.tx'); + expectBufferField(t, payload.cao.txv, 32, 'cao.txv'); + expectBufferField(t, payload.cao.iw, 32, 'cao.iw'); + t.ok(b4a.equals(payload.cao.txv, txValidity)); + t.ok(b4a.equals(payload.cao.iw, writingKey)); + expectBufferField(t, payload.cao.in, 32, 'cao.in'); + expectBufferField(t, payload.cao.is, 64, 'cao.is'); +}); + +test('ApplyStateMessageBuilder complete balance initialization (bio)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const otherWallet = await createWallet(testKeyPair2.mnemonic); + const txValidity = toBuf(hex('55', 32)); + const amount = toBuf(hex('66', 16)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.BALANCE_INITIALIZATION) + .setAddress(wallet.address) + .setIncomingAddress(otherWallet.address) + .setAmount(amount) + .setTxValidity(txValidity) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.BALANCE_INITIALIZATION); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'bio'); + expectKeys(t, payload.bio, ['tx', 'txv', 'ia', 'am', 'in', 'is'], 'bio'); + expectBufferField(t, payload.bio.tx, 32, 'bio.tx'); + expectBufferField(t, payload.bio.txv, 32, 'bio.txv'); + expectAddressBuffer(t, payload.bio.ia, 'bio.ia'); + expectBufferField(t, payload.bio.am, 16, 'bio.am'); + t.ok(b4a.equals(payload.bio.txv, txValidity)); + t.ok(b4a.equals(payload.bio.ia, addressToBuffer(otherWallet.address, config.addressPrefix))); + t.ok(b4a.equals(payload.bio.am, amount)); + expectBufferField(t, payload.bio.in, 32, 'bio.in'); + expectBufferField(t, payload.bio.is, 64, 'bio.is'); +}); + +test('ApplyStateMessageBuilder complete append whitelist (aco)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const otherWallet = await createWallet(testKeyPair2.mnemonic); + const txValidity = toBuf(hex('77', 32)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.APPEND_WHITELIST) + .setAddress(wallet.address) + .setIncomingAddress(otherWallet.address) + .setTxValidity(txValidity) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.APPEND_WHITELIST); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'aco'); + expectKeys(t, payload.aco, ['tx', 'txv', 'ia', 'in', 'is'], 'aco'); + expectBufferField(t, payload.aco.tx, 32, 'aco.tx'); + expectBufferField(t, payload.aco.txv, 32, 'aco.txv'); + expectAddressBuffer(t, payload.aco.ia, 'aco.ia'); + t.ok(b4a.equals(payload.aco.txv, txValidity)); + t.ok(b4a.equals(payload.aco.ia, addressToBuffer(otherWallet.address, config.addressPrefix))); + expectBufferField(t, payload.aco.in, 32, 'aco.in'); + expectBufferField(t, payload.aco.is, 64, 'aco.is'); +}); + +test('ApplyStateMessageBuilder complete add indexer (aco)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const otherWallet = await createWallet(testKeyPair2.mnemonic); + const txValidity = toBuf(hex('88', 32)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.ADD_INDEXER) + .setAddress(wallet.address) + .setIncomingAddress(otherWallet.address) + .setTxValidity(txValidity) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.ADD_INDEXER); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'aco'); + expectKeys(t, payload.aco, ['tx', 'txv', 'ia', 'in', 'is'], 'aco'); + expectBufferField(t, payload.aco.tx, 32, 'aco.tx'); + expectBufferField(t, payload.aco.txv, 32, 'aco.txv'); + expectAddressBuffer(t, payload.aco.ia, 'aco.ia'); + t.ok(b4a.equals(payload.aco.txv, txValidity)); + t.ok(b4a.equals(payload.aco.ia, addressToBuffer(otherWallet.address, config.addressPrefix))); + expectBufferField(t, payload.aco.in, 32, 'aco.in'); + expectBufferField(t, payload.aco.is, 64, 'aco.is'); +}); + +test('ApplyStateMessageBuilder complete remove indexer (aco)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const otherWallet = await createWallet(testKeyPair2.mnemonic); + const txValidity = toBuf(hex('99', 32)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.REMOVE_INDEXER) + .setAddress(wallet.address) + .setIncomingAddress(otherWallet.address) + .setTxValidity(txValidity) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.REMOVE_INDEXER); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'aco'); + expectKeys(t, payload.aco, ['tx', 'txv', 'ia', 'in', 'is'], 'aco'); + expectBufferField(t, payload.aco.tx, 32, 'aco.tx'); + expectBufferField(t, payload.aco.txv, 32, 'aco.txv'); + expectAddressBuffer(t, payload.aco.ia, 'aco.ia'); + t.ok(b4a.equals(payload.aco.txv, txValidity)); + t.ok(b4a.equals(payload.aco.ia, addressToBuffer(otherWallet.address, config.addressPrefix))); + expectBufferField(t, payload.aco.in, 32, 'aco.in'); + expectBufferField(t, payload.aco.is, 64, 'aco.is'); +}); + +test('ApplyStateMessageBuilder complete ban validator (aco)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const otherWallet = await createWallet(testKeyPair2.mnemonic); + const txValidity = toBuf(hex('aa', 32)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.BAN_VALIDATOR) + .setAddress(wallet.address) + .setIncomingAddress(otherWallet.address) + .setTxValidity(txValidity) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.BAN_VALIDATOR); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'aco'); + expectKeys(t, payload.aco, ['tx', 'txv', 'ia', 'in', 'is'], 'aco'); + expectBufferField(t, payload.aco.tx, 32, 'aco.tx'); + expectBufferField(t, payload.aco.txv, 32, 'aco.txv'); + expectAddressBuffer(t, payload.aco.ia, 'aco.ia'); + t.ok(b4a.equals(payload.aco.txv, txValidity)); + t.ok(b4a.equals(payload.aco.ia, addressToBuffer(otherWallet.address, config.addressPrefix))); + expectBufferField(t, payload.aco.in, 32, 'aco.in'); + expectBufferField(t, payload.aco.is, 64, 'aco.is'); +}); + +test('ApplyStateMessageBuilder complete add writer (rao)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txHash = toBuf(hex('bb', 32)); + const txValidity = toBuf(hex('cc', 32)); + const incomingWriterKey = toBuf(hex('dd', 32)); + const incomingNonce = toBuf(hex('ee', 32)); + const incomingSignature = toBuf(hex('ff', 64)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.ADD_WRITER) + .setAddress(wallet.address) + .setTxHash(txHash) + .setTxValidity(txValidity) + .setIncomingWriterKey(incomingWriterKey) + .setIncomingNonce(incomingNonce) + .setIncomingSignature(incomingSignature) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.ADD_WRITER); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'rao'); + expectKeys(t, payload.rao, ['tx', 'txv', 'iw', 'in', 'is', 'va', 'vn', 'vs'], 'rao'); + expectBufferField(t, payload.rao.tx, 32, 'rao.tx'); + expectBufferField(t, payload.rao.txv, 32, 'rao.txv'); + expectBufferField(t, payload.rao.iw, 32, 'rao.iw'); + expectBufferField(t, payload.rao.in, 32, 'rao.in'); + expectBufferField(t, payload.rao.is, 64, 'rao.is'); + expectAddressBuffer(t, payload.rao.va, 'rao.va'); + expectBufferField(t, payload.rao.vn, 32, 'rao.vn'); + expectBufferField(t, payload.rao.vs, 64, 'rao.vs'); + t.ok(b4a.equals(payload.rao.tx, txHash)); + t.ok(b4a.equals(payload.rao.txv, txValidity)); + t.ok(b4a.equals(payload.rao.iw, incomingWriterKey)); + t.ok(b4a.equals(payload.rao.in, incomingNonce)); + t.ok(b4a.equals(payload.rao.is, incomingSignature)); +}); + +test('ApplyStateMessageBuilder complete remove writer (rao)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txHash = toBuf(hex('01', 32)); + const txValidity = toBuf(hex('02', 32)); + const incomingWriterKey = toBuf(hex('03', 32)); + const incomingNonce = toBuf(hex('04', 32)); + const incomingSignature = toBuf(hex('05', 64)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.REMOVE_WRITER) + .setAddress(wallet.address) + .setTxHash(txHash) + .setTxValidity(txValidity) + .setIncomingWriterKey(incomingWriterKey) + .setIncomingNonce(incomingNonce) + .setIncomingSignature(incomingSignature) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.REMOVE_WRITER); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'rao'); + expectKeys(t, payload.rao, ['tx', 'txv', 'iw', 'in', 'is', 'va', 'vn', 'vs'], 'rao'); + expectBufferField(t, payload.rao.tx, 32, 'rao.tx'); + expectBufferField(t, payload.rao.txv, 32, 'rao.txv'); + expectBufferField(t, payload.rao.iw, 32, 'rao.iw'); + expectBufferField(t, payload.rao.in, 32, 'rao.in'); + expectBufferField(t, payload.rao.is, 64, 'rao.is'); + expectAddressBuffer(t, payload.rao.va, 'rao.va'); + expectBufferField(t, payload.rao.vn, 32, 'rao.vn'); + expectBufferField(t, payload.rao.vs, 64, 'rao.vs'); + t.ok(b4a.equals(payload.rao.tx, txHash)); + t.ok(b4a.equals(payload.rao.txv, txValidity)); + t.ok(b4a.equals(payload.rao.iw, incomingWriterKey)); + t.ok(b4a.equals(payload.rao.in, incomingNonce)); + t.ok(b4a.equals(payload.rao.is, incomingSignature)); +}); + +test('ApplyStateMessageBuilder complete admin recovery (rao)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txHash = toBuf(hex('10', 32)); + const txValidity = toBuf(hex('20', 32)); + const incomingWriterKey = toBuf(hex('30', 32)); + const incomingNonce = toBuf(hex('40', 32)); + const incomingSignature = toBuf(hex('50', 64)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.ADMIN_RECOVERY) + .setAddress(wallet.address) + .setTxHash(txHash) + .setTxValidity(txValidity) + .setIncomingWriterKey(incomingWriterKey) + .setIncomingNonce(incomingNonce) + .setIncomingSignature(incomingSignature) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.ADMIN_RECOVERY); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'rao'); + expectKeys(t, payload.rao, ['tx', 'txv', 'iw', 'in', 'is', 'va', 'vn', 'vs'], 'rao'); + expectBufferField(t, payload.rao.tx, 32, 'rao.tx'); + expectBufferField(t, payload.rao.txv, 32, 'rao.txv'); + expectBufferField(t, payload.rao.iw, 32, 'rao.iw'); + expectBufferField(t, payload.rao.in, 32, 'rao.in'); + expectBufferField(t, payload.rao.is, 64, 'rao.is'); + expectAddressBuffer(t, payload.rao.va, 'rao.va'); + expectBufferField(t, payload.rao.vn, 32, 'rao.vn'); + expectBufferField(t, payload.rao.vs, 64, 'rao.vs'); + t.ok(b4a.equals(payload.rao.tx, txHash)); + t.ok(b4a.equals(payload.rao.txv, txValidity)); + t.ok(b4a.equals(payload.rao.iw, incomingWriterKey)); + t.ok(b4a.equals(payload.rao.in, incomingNonce)); + t.ok(b4a.equals(payload.rao.is, incomingSignature)); +}); + +test('ApplyStateMessageBuilder complete bootstrap deployment (bdo)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txHash = toBuf(hex('60', 32)); + const txValidity = toBuf(hex('70', 32)); + const externalBootstrap = toBuf(hex('80', 32)); + const channel = toBuf(hex('90', 32)); + const incomingNonce = toBuf(hex('a0', 32)); + const incomingSignature = toBuf(hex('b0', 64)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.BOOTSTRAP_DEPLOYMENT) + .setAddress(wallet.address) + .setTxHash(txHash) + .setTxValidity(txValidity) + .setExternalBootstrap(externalBootstrap) + .setChannel(channel) + .setIncomingNonce(incomingNonce) + .setIncomingSignature(incomingSignature) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.BOOTSTRAP_DEPLOYMENT); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'bdo'); + expectKeys(t, payload.bdo, ['tx', 'txv', 'bs', 'ic', 'in', 'is', 'va', 'vn', 'vs'], 'bdo'); + expectBufferField(t, payload.bdo.tx, 32, 'bdo.tx'); + expectBufferField(t, payload.bdo.txv, 32, 'bdo.txv'); + expectBufferField(t, payload.bdo.bs, 32, 'bdo.bs'); + expectBufferField(t, payload.bdo.ic, 32, 'bdo.ic'); + expectBufferField(t, payload.bdo.in, 32, 'bdo.in'); + expectBufferField(t, payload.bdo.is, 64, 'bdo.is'); + expectAddressBuffer(t, payload.bdo.va, 'bdo.va'); + expectBufferField(t, payload.bdo.vn, 32, 'bdo.vn'); + expectBufferField(t, payload.bdo.vs, 64, 'bdo.vs'); + t.ok(b4a.equals(payload.bdo.tx, txHash)); + t.ok(b4a.equals(payload.bdo.txv, txValidity)); + t.ok(b4a.equals(payload.bdo.bs, externalBootstrap)); + t.ok(b4a.equals(payload.bdo.ic, channel)); + t.ok(b4a.equals(payload.bdo.in, incomingNonce)); + t.ok(b4a.equals(payload.bdo.is, incomingSignature)); +}); + +test('ApplyStateMessageBuilder complete transaction operation (txo)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txHash = toBuf(hex('c0', 32)); + const txValidity = toBuf(hex('d0', 32)); + const incomingWriterKey = toBuf(hex('e0', 32)); + const incomingNonce = toBuf(hex('f0', 32)); + const incomingSignature = toBuf(hex('01', 64)); + const contentHash = toBuf(hex('02', 32)); + const externalBootstrap = toBuf(hex('03', 32)); + const msbBootstrap = toBuf(hex('04', 32)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.TX) + .setAddress(wallet.address) + .setTxHash(txHash) + .setTxValidity(txValidity) + .setIncomingWriterKey(incomingWriterKey) + .setIncomingNonce(incomingNonce) + .setIncomingSignature(incomingSignature) + .setContentHash(contentHash) + .setExternalBootstrap(externalBootstrap) + .setMsbBootstrap(msbBootstrap) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.TX); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'txo'); + expectKeys(t, payload.txo, ['tx', 'txv', 'iw', 'ch', 'bs', 'mbs', 'in', 'is', 'va', 'vn', 'vs'], 'txo'); + expectBufferField(t, payload.txo.tx, 32, 'txo.tx'); + expectBufferField(t, payload.txo.txv, 32, 'txo.txv'); + expectBufferField(t, payload.txo.iw, 32, 'txo.iw'); + expectBufferField(t, payload.txo.in, 32, 'txo.in'); + expectBufferField(t, payload.txo.is, 64, 'txo.is'); + expectBufferField(t, payload.txo.ch, 32, 'txo.ch'); + expectBufferField(t, payload.txo.bs, 32, 'txo.bs'); + expectBufferField(t, payload.txo.mbs, 32, 'txo.mbs'); + expectAddressBuffer(t, payload.txo.va, 'txo.va'); + expectBufferField(t, payload.txo.vn, 32, 'txo.vn'); + expectBufferField(t, payload.txo.vs, 64, 'txo.vs'); + t.ok(b4a.equals(payload.txo.tx, txHash)); + t.ok(b4a.equals(payload.txo.txv, txValidity)); + t.ok(b4a.equals(payload.txo.iw, incomingWriterKey)); + t.ok(b4a.equals(payload.txo.in, incomingNonce)); + t.ok(b4a.equals(payload.txo.is, incomingSignature)); + t.ok(b4a.equals(payload.txo.ch, contentHash)); + t.ok(b4a.equals(payload.txo.bs, externalBootstrap)); + t.ok(b4a.equals(payload.txo.mbs, msbBootstrap)); +}); + +test('ApplyStateMessageBuilder complete transfer operation (tro)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const otherWallet = await createWallet(testKeyPair2.mnemonic); + const txHash = toBuf(hex('05', 32)); + const txValidity = toBuf(hex('06', 32)); + const incomingNonce = toBuf(hex('07', 32)); + const incomingSignature = toBuf(hex('08', 64)); + const amount = toBuf(hex('09', 16)); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('complete') + .setOutput('buffer') + .setOperationType(OperationType.TRANSFER) + .setAddress(wallet.address) + .setTxHash(txHash) + .setTxValidity(txValidity) + .setIncomingNonce(incomingNonce) + .setIncomingAddress(otherWallet.address) + .setAmount(amount) + .setIncomingSignature(incomingSignature) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.TRANSFER); + expectAddressBuffer(t, payload.address, 'address'); + t.ok(b4a.equals(payload.address, addressToBuffer(wallet.address, config.addressPrefix))); + expectPayloadKeys(t, payload, 'tro'); + expectKeys(t, payload.tro, ['tx', 'txv', 'to', 'am', 'in', 'is', 'va', 'vn', 'vs'], 'tro'); + expectBufferField(t, payload.tro.tx, 32, 'tro.tx'); + expectBufferField(t, payload.tro.txv, 32, 'tro.txv'); + expectAddressBuffer(t, payload.tro.to, 'tro.to'); + expectBufferField(t, payload.tro.am, 16, 'tro.am'); + expectBufferField(t, payload.tro.in, 32, 'tro.in'); + expectBufferField(t, payload.tro.is, 64, 'tro.is'); + expectAddressBuffer(t, payload.tro.va, 'tro.va'); + expectBufferField(t, payload.tro.vn, 32, 'tro.vn'); + expectBufferField(t, payload.tro.vs, 64, 'tro.vs'); + t.ok(b4a.equals(payload.tro.tx, txHash)); + t.ok(b4a.equals(payload.tro.txv, txValidity)); + t.ok(b4a.equals(payload.tro.to, addressToBuffer(otherWallet.address, config.addressPrefix))); + t.ok(b4a.equals(payload.tro.am, amount)); + t.ok(b4a.equals(payload.tro.in, incomingNonce)); + t.ok(b4a.equals(payload.tro.is, incomingSignature)); +}); diff --git a/tests/unit/messages/state/applyStateMessageBuilder.partial.test.js b/tests/unit/messages/state/applyStateMessageBuilder.partial.test.js new file mode 100644 index 00000000..0104bea4 --- /dev/null +++ b/tests/unit/messages/state/applyStateMessageBuilder.partial.test.js @@ -0,0 +1,233 @@ +import { test } from 'brittle'; +import b4a from 'b4a'; +import PeerWallet from 'trac-wallet'; + +import ApplyStateMessageBuilder from '../../../../src/messages/state/ApplyStateMessageBuilder.js'; +import { OperationType } from '../../../../src/utils/constants.js'; +import { isHexString } from '../../../../src/utils/helpers.js'; +import { config } from '../../../helpers/config.js'; +import { testKeyPair1, testKeyPair2 } from '../../../fixtures/apply.fixtures.js'; +import { isAddressValid } from '../../../../src/core/state/utils/address.js'; + +const hex = (value, bytes) => value.repeat(bytes); + +async function createWallet(mnemonic) { + const wallet = new PeerWallet({ mnemonic, networkPrefix: config.addressPrefix }); + await wallet.ready; + return wallet; +} + +function expectHexField(t, value, bytes, label) { + t.is(typeof value, 'string', `${label} type`); + t.is(value.length, bytes * 2, `${label} length`); + t.ok(isHexString(value), `${label} hex`); +} + +function expectAddressField(t, value, label) { + t.is(typeof value, 'string', `${label} type`); + t.is(value.length, config.addressLength, `${label} length`); + t.ok(isAddressValid(value, config.addressPrefix), `${label} valid`); +} + +function expectKeys(t, value, keys, label) { + t.alike(Object.keys(value).sort(), keys.slice().sort(), `${label} keys`); +} + +function expectPayloadKeys(t, payload, bodyKey) { + expectKeys(t, payload, ['type', 'address', bodyKey], 'payload'); +} + +test('ApplyStateMessageBuilder partial add writer (rao)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txValidity = hex('11', 32); + const writingKey = hex('22', 32); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('partial') + .setOutput('json') + .setOperationType(OperationType.ADD_WRITER) + .setAddress(wallet.address) + .setTxValidity(txValidity) + .setWriterKey(writingKey) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.ADD_WRITER); + t.is(payload.address, wallet.address); + expectAddressField(t, payload.address, 'address'); + expectPayloadKeys(t, payload, 'rao'); + expectKeys(t, payload.rao, ['tx', 'txv', 'iw', 'in', 'is'], 'rao'); + expectHexField(t, payload.rao.tx, 32, 'rao.tx'); + expectHexField(t, payload.rao.txv, 32, 'rao.txv'); + expectHexField(t, payload.rao.iw, 32, 'rao.iw'); + expectHexField(t, payload.rao.in, 32, 'rao.in'); + expectHexField(t, payload.rao.is, 64, 'rao.is'); + t.is(payload.rao.txv, txValidity); + t.is(payload.rao.iw, writingKey); +}); + +test('ApplyStateMessageBuilder partial remove writer (rao)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txValidity = hex('33', 32); + const writingKey = hex('44', 32); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('partial') + .setOutput('json') + .setOperationType(OperationType.REMOVE_WRITER) + .setAddress(wallet.address) + .setTxValidity(txValidity) + .setWriterKey(writingKey) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.REMOVE_WRITER); + t.is(payload.address, wallet.address); + expectPayloadKeys(t, payload, 'rao'); + expectKeys(t, payload.rao, ['tx', 'txv', 'iw', 'in', 'is'], 'rao'); + expectHexField(t, payload.rao.tx, 32, 'rao.tx'); + expectHexField(t, payload.rao.txv, 32, 'rao.txv'); + expectHexField(t, payload.rao.iw, 32, 'rao.iw'); + expectHexField(t, payload.rao.in, 32, 'rao.in'); + expectHexField(t, payload.rao.is, 64, 'rao.is'); + t.is(payload.rao.txv, txValidity); + t.is(payload.rao.iw, writingKey); +}); + +test('ApplyStateMessageBuilder partial admin recovery (rao)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txValidity = hex('55', 32); + const writingKey = hex('66', 32); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('partial') + .setOutput('json') + .setOperationType(OperationType.ADMIN_RECOVERY) + .setAddress(wallet.address) + .setTxValidity(txValidity) + .setWriterKey(writingKey) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.ADMIN_RECOVERY); + t.is(payload.address, wallet.address); + expectPayloadKeys(t, payload, 'rao'); + expectKeys(t, payload.rao, ['tx', 'txv', 'iw', 'in', 'is'], 'rao'); + expectHexField(t, payload.rao.tx, 32, 'rao.tx'); + expectHexField(t, payload.rao.txv, 32, 'rao.txv'); + expectHexField(t, payload.rao.iw, 32, 'rao.iw'); + expectHexField(t, payload.rao.in, 32, 'rao.in'); + expectHexField(t, payload.rao.is, 64, 'rao.is'); + t.is(payload.rao.txv, txValidity); + t.is(payload.rao.iw, writingKey); +}); + +test('ApplyStateMessageBuilder partial bootstrap deployment (bdo)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txValidity = hex('77', 32); + const externalBootstrap = hex('88', 32); + const channel = hex('99', 32); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('partial') + .setOutput('json') + .setOperationType(OperationType.BOOTSTRAP_DEPLOYMENT) + .setAddress(wallet.address) + .setTxValidity(txValidity) + .setExternalBootstrap(externalBootstrap) + .setChannel(channel) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.BOOTSTRAP_DEPLOYMENT); + t.is(payload.address, wallet.address); + expectPayloadKeys(t, payload, 'bdo'); + expectKeys(t, payload.bdo, ['tx', 'txv', 'bs', 'ic', 'in', 'is'], 'bdo'); + expectHexField(t, payload.bdo.tx, 32, 'bdo.tx'); + expectHexField(t, payload.bdo.txv, 32, 'bdo.txv'); + expectHexField(t, payload.bdo.bs, 32, 'bdo.bs'); + expectHexField(t, payload.bdo.ic, 32, 'bdo.ic'); + expectHexField(t, payload.bdo.in, 32, 'bdo.in'); + expectHexField(t, payload.bdo.is, 64, 'bdo.is'); + t.is(payload.bdo.txv, txValidity); + t.is(payload.bdo.bs, externalBootstrap); + t.is(payload.bdo.ic, channel); +}); + +test('ApplyStateMessageBuilder partial transaction operation (txo)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const txValidity = hex('aa', 32); + const writingKey = hex('bb', 32); + const contentHash = hex('cc', 32); + const externalBootstrap = hex('dd', 32); + const msbBootstrap = hex('ee', 32); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('partial') + .setOutput('json') + .setOperationType(OperationType.TX) + .setAddress(wallet.address) + .setTxValidity(txValidity) + .setWriterKey(writingKey) + .setContentHash(contentHash) + .setExternalBootstrap(externalBootstrap) + .setMsbBootstrap(msbBootstrap) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.TX); + t.is(payload.address, wallet.address); + expectPayloadKeys(t, payload, 'txo'); + expectKeys(t, payload.txo, ['tx', 'txv', 'iw', 'ch', 'bs', 'mbs', 'in', 'is'], 'txo'); + expectHexField(t, payload.txo.tx, 32, 'txo.tx'); + expectHexField(t, payload.txo.txv, 32, 'txo.txv'); + expectHexField(t, payload.txo.iw, 32, 'txo.iw'); + expectHexField(t, payload.txo.ch, 32, 'txo.ch'); + expectHexField(t, payload.txo.bs, 32, 'txo.bs'); + expectHexField(t, payload.txo.mbs, 32, 'txo.mbs'); + expectHexField(t, payload.txo.in, 32, 'txo.in'); + expectHexField(t, payload.txo.is, 64, 'txo.is'); + t.is(payload.txo.txv, txValidity); + t.is(payload.txo.iw, writingKey); + t.is(payload.txo.ch, contentHash); + t.is(payload.txo.bs, externalBootstrap); + t.is(payload.txo.mbs, msbBootstrap); +}); + +test('ApplyStateMessageBuilder partial transfer operation (tro)', async t => { + const wallet = await createWallet(testKeyPair1.mnemonic); + const otherWallet = await createWallet(testKeyPair2.mnemonic); + const txValidity = hex('ab', 32); + const amount = hex('cd', 16); + + const builder = new ApplyStateMessageBuilder(wallet, config); + await builder + .setPhase('partial') + .setOutput('json') + .setOperationType(OperationType.TRANSFER) + .setAddress(wallet.address) + .setTxValidity(txValidity) + .setIncomingAddress(otherWallet.address) + .setAmount(amount) + .build(); + + const payload = builder.getPayload(); + t.is(payload.type, OperationType.TRANSFER); + t.is(payload.address, wallet.address); + expectPayloadKeys(t, payload, 'tro'); + expectKeys(t, payload.tro, ['tx', 'txv', 'to', 'am', 'in', 'is'], 'tro'); + expectHexField(t, payload.tro.tx, 32, 'tro.tx'); + expectHexField(t, payload.tro.txv, 32, 'tro.txv'); + expectAddressField(t, payload.tro.to, 'tro.to'); + expectHexField(t, payload.tro.am, 16, 'tro.am'); + expectHexField(t, payload.tro.in, 32, 'tro.in'); + expectHexField(t, payload.tro.is, 64, 'tro.is'); + t.is(payload.tro.txv, txValidity); + t.is(payload.tro.to, otherWallet.address); + t.is(payload.tro.am, amount); +}); diff --git a/tests/unit/network/networkModule.test.js b/tests/unit/network/networkModule.test.js index d25bb07d..13ae9b9e 100644 --- a/tests/unit/network/networkModule.test.js +++ b/tests/unit/network/networkModule.test.js @@ -1,9 +1,10 @@ import { default as test } from 'brittle'; -async function runConnectionManagerTests() { +async function runNetworkModuleTests() { test.pause(); await import('./ConnectionManager.test.js'); + await import('./NetworkWalletFactory.test.js'); test.resume(); } -runConnectionManagerTests(); \ No newline at end of file +await runNetworkModuleTests(); diff --git a/tests/unit/state/apply/addAdmin/addAdminHappyPathScenario.js b/tests/unit/state/apply/addAdmin/addAdminHappyPathScenario.js index af5017e7..a301610a 100644 --- a/tests/unit/state/apply/addAdmin/addAdminHappyPathScenario.js +++ b/tests/unit/state/apply/addAdmin/addAdminHappyPathScenario.js @@ -5,7 +5,8 @@ import { } from '../../../../helpers/autobaseTestHelpers.js'; import nodeEntryUtils from '../../../../../src/core/state/utils/nodeEntry.js'; import { toTerm } from '../../../../../src/core/state/utils/balance.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import { setupAddAdminScenario, assertAdminState } from './addAdminScenarioHelpers.js'; import { config } from '../../../../helpers/config.js'; @@ -17,11 +18,14 @@ export default function addAdminHappyPathScenario() { const reader = readerNodes[0]; const txValidity = await deriveIndexerSequenceState(adminNode.base); - const addAdminPayload = await new CompleteStateMessageOperations(adminNode.wallet, config) - .assembleAddAdminMessage( - adminNode.base.local.key, - txValidity - ); + const addAdminPayload = safeEncodeApplyOperation( + await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteAddAdminMessage( + adminNode.wallet.address, + adminNode.base.local.key, + txValidity + ) + ); await adminNode.base.append(addAdminPayload); await adminNode.base.update(); diff --git a/tests/unit/state/apply/addAdmin/addAdminScenarioHelpers.js b/tests/unit/state/apply/addAdmin/addAdminScenarioHelpers.js index 27286f33..c49e4f1c 100644 --- a/tests/unit/state/apply/addAdmin/addAdminScenarioHelpers.js +++ b/tests/unit/state/apply/addAdmin/addAdminScenarioHelpers.js @@ -6,7 +6,7 @@ import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; import { safeDecodeApplyOperation, safeEncodeApplyOperation @@ -42,11 +42,14 @@ export async function setupAddAdminScenario(t) { export async function buildAddAdminRequesterPayload(context) { const adminNode = context.adminBootstrap; const txValidity = await deriveIndexerSequenceState(adminNode.base); - return new CompleteStateMessageOperations(adminNode.wallet, config) - .assembleAddAdminMessage( - adminNode.base.local.key, - txValidity - ); + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteAddAdminMessage( + adminNode.wallet.address, + adminNode.base.local.key, + txValidity + ) + ); } export async function assertAddAdminRequesterFailureState(t, context) { diff --git a/tests/unit/state/apply/addAdmin/state.apply.addAdmin.test.js b/tests/unit/state/apply/addAdmin/state.apply.addAdmin.test.js index 3b6c3d80..dbd34229 100644 --- a/tests/unit/state/apply/addAdmin/state.apply.addAdmin.test.js +++ b/tests/unit/state/apply/addAdmin/state.apply.addAdmin.test.js @@ -18,7 +18,8 @@ import WriterKeyExistsValidationScenario from '../common/writerKeyExistsValidati import OperationAlreadyAppliedScenario from '../common/operationAlreadyAppliedScenario.js'; import TransactionValidityMismatchScenario from '../common/transactionValidityMismatchScenario.js'; import IndexerSequenceStateInvalidScenario from '../common/indexer/indexerSequenceStateInvalidScenario.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import addAdminEntryExistsScenario from './adminEntryExistsScenario.js'; import addAdminNonBootstrapNodeScenario from './nonBootstrapNodeScenario.js'; import addAdminNodeEntryInitializationFailureScenario from './nodeEntryInitializationFailureScenario.js'; @@ -127,13 +128,15 @@ new TransactionValidityMismatchScenario({ setupScenario: setupAddAdminScenario, buildValidPayload: buildAddAdminRequesterPayload, assertStateUnchanged: assertAddAdminRequesterFailureState, - rebuildPayloadWithTxValidity: ({ context, mutatedTxValidity }) => { + rebuildPayloadWithTxValidity: async ({ context, mutatedTxValidity }) => { const adminNode = context.adminBootstrap; - return new CompleteStateMessageOperations(adminNode.wallet, config) - .assembleAddAdminMessage( - adminNode.base.local.key, - mutatedTxValidity - ); + const payload = await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteAddAdminMessage( + adminNode.wallet.address, + adminNode.base.local.key, + mutatedTxValidity + ); + return safeEncodeApplyOperation(payload); }, expectedLogs: ['Transaction was not executed.'] }).performScenario(); diff --git a/tests/unit/state/apply/addIndexer/addIndexerScenarioHelpers.js b/tests/unit/state/apply/addIndexer/addIndexerScenarioHelpers.js index 39499d50..f46f1e49 100644 --- a/tests/unit/state/apply/addIndexer/addIndexerScenarioHelpers.js +++ b/tests/unit/state/apply/addIndexer/addIndexerScenarioHelpers.js @@ -1,5 +1,6 @@ import b4a from 'b4a'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; import { setupAddWriterScenario, @@ -51,11 +52,10 @@ export async function buildAddIndexerPayload( } const txValidity = await deriveIndexerSequenceState(adminPeer.base); - return new CompleteStateMessageOperations(adminPeer.wallet, config) - .assembleAddIndexerMessage( - writerPeer.wallet.address, - txValidity - ); + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminPeer.wallet, config) + .buildCompleteAddIndexerMessage(adminPeer.wallet.address, writerPeer.wallet.address, txValidity) + ); } export async function applyWithIndexerRoleUpdateFailure(context, invalidPayload) { @@ -99,11 +99,10 @@ export async function buildAddIndexerPayloadWithTxValidity( throw new Error('buildAddIndexerPayloadWithTxValidity requires an admin peer.'); } - return new CompleteStateMessageOperations(adminPeer.wallet, config) - .assembleAddIndexerMessage( - writerPeer.wallet.address, - mutatedTxValidity - ); + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminPeer.wallet, config) + .buildCompleteAddIndexerMessage(adminPeer.wallet.address, writerPeer.wallet.address, mutatedTxValidity) + ); } export function ensureIndexerRegistration(base, writingKey) { @@ -147,11 +146,10 @@ export async function buildRemoveIndexerPayload( } const txValidity = await deriveIndexerSequenceState(adminPeer.base); - return new CompleteStateMessageOperations(adminPeer.wallet, config) - .assembleRemoveIndexerMessage( - indexerPeer.wallet.address, - txValidity - ); + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminPeer.wallet, config) + .buildCompleteRemoveIndexerMessage(adminPeer.wallet.address, indexerPeer.wallet.address, txValidity) + ); } export async function buildRemoveIndexerPayloadWithTxValidity( @@ -169,11 +167,10 @@ export async function buildRemoveIndexerPayloadWithTxValidity( throw new Error('buildRemoveIndexerPayloadWithTxValidity requires an admin peer.'); } - return new CompleteStateMessageOperations(adminPeer.wallet, config) - .assembleRemoveIndexerMessage( - indexerPeer.wallet.address, - mutatedTxValidity - ); + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminPeer.wallet, config) + .buildCompleteRemoveIndexerMessage(adminPeer.wallet.address, indexerPeer.wallet.address, mutatedTxValidity) + ); } export async function assertAddIndexerSuccessState( diff --git a/tests/unit/state/apply/addWriter/addWriterScenarioHelpers.js b/tests/unit/state/apply/addWriter/addWriterScenarioHelpers.js index fd1758d6..478efafc 100644 --- a/tests/unit/state/apply/addWriter/addWriterScenarioHelpers.js +++ b/tests/unit/state/apply/addWriter/addWriterScenarioHelpers.js @@ -1,6 +1,6 @@ import b4a from 'b4a'; -import PartialStateMessageOperations from '../../../../../src/messages/partialStateMessages/PartialStateMessageOperations.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; import { safeDecodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import nodeEntryUtils, { ZERO_LICENSE } from '../../../../../src/core/state/utils/nodeEntry.js'; @@ -105,19 +105,24 @@ export async function buildAddWriterPayload( ) { const txValidity = await deriveIndexerSequenceState(validatorPeer.base); const writingKey = writerKeyBuffer ?? readerPeer.base.local.key; - const partial = await new PartialStateMessageOperations(readerPeer.wallet, config).assembleAddWriterMessage( - writingKey.toString('hex'), - txValidity.toString('hex') - ); + const partial = await applyStateMessageFactory(readerPeer.wallet, config) + .buildPartialAddWriterMessage( + readerPeer.wallet.address, + writingKey.toString('hex'), + txValidity.toString('hex'), + 'json' + ); - return new CompleteStateMessageOperations(validatorPeer.wallet, config).assembleAddWriterMessage( - partial.address, - b4a.from(partial.rao.tx, 'hex'), - b4a.from(partial.rao.txv, 'hex'), - b4a.from(partial.rao.iw, 'hex'), - b4a.from(partial.rao.in, 'hex'), - b4a.from(partial.rao.is, 'hex') - ); + const payload = await applyStateMessageFactory(validatorPeer.wallet, config) + .buildCompleteAddWriterMessage( + partial.address, + b4a.from(partial.rao.tx, 'hex'), + b4a.from(partial.rao.txv, 'hex'), + b4a.from(partial.rao.iw, 'hex'), + b4a.from(partial.rao.in, 'hex'), + b4a.from(partial.rao.is, 'hex') + ); + return safeEncodeApplyOperation(payload); } export async function buildAddWriterPayloadWithTxValidity( @@ -134,19 +139,24 @@ export async function buildAddWriterPayloadWithTxValidity( } const writingKey = writerKeyBuffer ?? readerPeer.base.local.key; - const partial = await new PartialStateMessageOperations(readerPeer.wallet, config).assembleAddWriterMessage( - writingKey.toString('hex'), - mutatedTxValidity.toString('hex') - ); + const partial = await applyStateMessageFactory(readerPeer.wallet, config) + .buildPartialAddWriterMessage( + readerPeer.wallet.address, + writingKey.toString('hex'), + mutatedTxValidity.toString('hex'), + 'json' + ); - return new CompleteStateMessageOperations(validatorPeer.wallet, config).assembleAddWriterMessage( - partial.address, - b4a.from(partial.rao.tx, 'hex'), - mutatedTxValidity, - b4a.from(partial.rao.iw, 'hex'), - b4a.from(partial.rao.in, 'hex'), - b4a.from(partial.rao.is, 'hex') - ); + const payload = await applyStateMessageFactory(validatorPeer.wallet, config) + .buildCompleteAddWriterMessage( + partial.address, + b4a.from(partial.rao.tx, 'hex'), + mutatedTxValidity, + b4a.from(partial.rao.iw, 'hex'), + b4a.from(partial.rao.in, 'hex'), + b4a.from(partial.rao.is, 'hex') + ); + return safeEncodeApplyOperation(payload); } export async function buildRemoveWriterPayload( @@ -159,19 +169,24 @@ export async function buildRemoveWriterPayload( ) { const txValidity = await deriveIndexerSequenceState(validatorPeer.base); const writerKey = writerKeyBuffer ?? readerPeer.base.local.key; - const partial = await new PartialStateMessageOperations(readerPeer.wallet, config).assembleRemoveWriterMessage( - writerKey.toString('hex'), - txValidity.toString('hex') - ); + const partial = await applyStateMessageFactory(readerPeer.wallet, config) + .buildPartialRemoveWriterMessage( + readerPeer.wallet.address, + writerKey.toString('hex'), + txValidity.toString('hex'), + 'json' + ); - return new CompleteStateMessageOperations(validatorPeer.wallet, config).assembleRemoveWriterMessage( - partial.address, - b4a.from(partial.rao.tx, 'hex'), - b4a.from(partial.rao.txv, 'hex'), - b4a.from(partial.rao.iw, 'hex'), - b4a.from(partial.rao.in, 'hex'), - b4a.from(partial.rao.is, 'hex') - ); + const payload = await applyStateMessageFactory(validatorPeer.wallet, config) + .buildCompleteRemoveWriterMessage( + partial.address, + b4a.from(partial.rao.tx, 'hex'), + b4a.from(partial.rao.txv, 'hex'), + b4a.from(partial.rao.iw, 'hex'), + b4a.from(partial.rao.in, 'hex'), + b4a.from(partial.rao.is, 'hex') + ); + return safeEncodeApplyOperation(payload); } export async function assertAddWriterSuccessState( diff --git a/tests/unit/state/apply/adminRecovery/adminRecoveryScenarioHelpers.js b/tests/unit/state/apply/adminRecovery/adminRecoveryScenarioHelpers.js index 0e164962..1d2e6078 100644 --- a/tests/unit/state/apply/adminRecovery/adminRecoveryScenarioHelpers.js +++ b/tests/unit/state/apply/adminRecovery/adminRecoveryScenarioHelpers.js @@ -4,8 +4,8 @@ import nodeEntryUtils, { setWritingKey } from '../../../../../src/core/state/uti import { EntryType } from '../../../../../src/utils/constants.js'; import { decimalStringToBigInt, bigIntTo16ByteBuffer } from '../../../../../src/utils/amountSerialization.js'; import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; -import PartialStateMessageOperations from '../../../../../src/messages/partialStateMessages/PartialStateMessageOperations.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import { setupAdminNetwork, initializeBalances, @@ -90,21 +90,24 @@ export async function buildAdminRecoveryPayload(context) { const { adminPeer, validatorPeer1, newAdminWriterKey } = context.adminRecovery; const txValidity = await deriveIndexerSequenceState(validatorPeer1.base); - const partial = await new PartialStateMessageOperations(adminPeer.wallet, config) - .assembleAdminRecoveryMessage( + const partial = await applyStateMessageFactory(adminPeer.wallet, config) + .buildPartialAdminRecoveryMessage( + adminPeer.wallet.address, b4a.toString(newAdminWriterKey, 'hex'), - b4a.toString(txValidity, 'hex') + b4a.toString(txValidity, 'hex'), + 'json' ); - return new CompleteStateMessageOperations(validatorPeer1.wallet, config) - .assembleAdminRecoveryMessage( - partial.address, - b4a.from(partial.rao.tx, 'hex'), - b4a.from(partial.rao.txv, 'hex'), - b4a.from(partial.rao.iw, 'hex'), - b4a.from(partial.rao.in, 'hex'), - b4a.from(partial.rao.is, 'hex') - ); + const payload = await applyStateMessageFactory(validatorPeer1.wallet, config) + .buildCompleteAdminRecoveryMessage( + partial.address, + b4a.from(partial.rao.tx, 'hex'), + b4a.from(partial.rao.txv, 'hex'), + b4a.from(partial.rao.iw, 'hex'), + b4a.from(partial.rao.in, 'hex'), + b4a.from(partial.rao.is, 'hex') + ); + return safeEncodeApplyOperation(payload); } export async function buildAdminRecoveryPayloadWithTxValidity(context, mutatedTxValidity) { @@ -113,21 +116,24 @@ export async function buildAdminRecoveryPayloadWithTxValidity(context, mutatedTx } const { adminPeer, validatorPeer1, newAdminWriterKey } = context.adminRecovery; - const partial = await new PartialStateMessageOperations(adminPeer.wallet, config) - .assembleAdminRecoveryMessage( + const partial = await applyStateMessageFactory(adminPeer.wallet, config) + .buildPartialAdminRecoveryMessage( + adminPeer.wallet.address, b4a.toString(newAdminWriterKey, 'hex'), - b4a.toString(mutatedTxValidity, 'hex') + b4a.toString(mutatedTxValidity, 'hex'), + 'json' ); - return new CompleteStateMessageOperations(validatorPeer1.wallet, config) - .assembleAdminRecoveryMessage( - partial.address, + const payload = await applyStateMessageFactory(validatorPeer1.wallet, config) + .buildCompleteAdminRecoveryMessage( + partial.address, b4a.from(partial.rao.tx, 'hex'), mutatedTxValidity, b4a.from(partial.rao.iw, 'hex'), b4a.from(partial.rao.in, 'hex'), b4a.from(partial.rao.is, 'hex') ); + return safeEncodeApplyOperation(payload); } export async function applyAdminRecovery(context, payload) { @@ -516,21 +522,26 @@ export async function applyTransferSeries(context, count = TRANSFER_COUNT) { async function buildSimpleTransferPayload({ requesterPeer, validatorPeer, recipientPeer, amount }) { const txValidity = await deriveIndexerSequenceState(validatorPeer.base); - const partial = await new PartialStateMessageOperations(requesterPeer.wallet, config).assembleTransferOperationMessage( - recipientPeer.wallet.address, - b4a.toString(amount, 'hex'), - b4a.toString(txValidity, 'hex') - ); - - return new CompleteStateMessageOperations(validatorPeer.wallet, config).assembleCompleteTransferOperationMessage( - partial.address, - b4a.from(partial.tro.tx, 'hex'), - b4a.from(partial.tro.txv, 'hex'), - b4a.from(partial.tro.in, 'hex'), - partial.tro.to, - b4a.from(partial.tro.am, 'hex'), - b4a.from(partial.tro.is, 'hex') - ); + const partial = await applyStateMessageFactory(requesterPeer.wallet, config) + .buildPartialTransferOperationMessage( + requesterPeer.wallet.address, + recipientPeer.wallet.address, + b4a.toString(amount, 'hex'), + b4a.toString(txValidity, 'hex'), + 'json' + ); + + const payload = await applyStateMessageFactory(validatorPeer.wallet, config) + .buildCompleteTransferOperationMessage( + partial.address, + b4a.from(partial.tro.tx, 'hex'), + b4a.from(partial.tro.txv, 'hex'), + b4a.from(partial.tro.in, 'hex'), + partial.tro.to, + b4a.from(partial.tro.am, 'hex'), + b4a.from(partial.tro.is, 'hex') + ); + return safeEncodeApplyOperation(payload); } export async function assertAdminRecoverySuccessState(t, context, { viewBase } = {}) { diff --git a/tests/unit/state/apply/appendWhitelist/appendWhitelistScenarioHelpers.js b/tests/unit/state/apply/appendWhitelist/appendWhitelistScenarioHelpers.js index 9c7ba805..a677c8f3 100644 --- a/tests/unit/state/apply/appendWhitelist/appendWhitelistScenarioHelpers.js +++ b/tests/unit/state/apply/appendWhitelist/appendWhitelistScenarioHelpers.js @@ -6,7 +6,7 @@ import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; import { AUTOBASE_VALUE_ENCODING, EntryType } from '../../../../../src/utils/constants.js'; import { safeDecodeApplyOperation, safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import nodeEntryUtils, { ZERO_LICENSE } from '../../../../../src/core/state/utils/nodeEntry.js'; @@ -45,11 +45,10 @@ export async function buildAppendWhitelistPayload(context, readerAddress = null) const adminNode = context.adminBootstrap; const targetAddress = readerAddress ?? selectReaderPeer(context).wallet.address; const txValidity = await deriveIndexerSequenceState(adminNode.base); - return new CompleteStateMessageOperations(adminNode.wallet, config) - .assembleAppendWhitelistMessages( - txValidity, - targetAddress - ); + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteAppendWhitelistMessage(adminNode.wallet.address, targetAddress, txValidity) + ); } export async function buildAppendWhitelistPayloadWithTxValidity( @@ -59,21 +58,19 @@ export async function buildAppendWhitelistPayloadWithTxValidity( ) { const adminNode = context.adminBootstrap; const targetAddress = readerAddress ?? selectReaderPeer(context).wallet.address; - return new CompleteStateMessageOperations(adminNode.wallet, config) - .assembleAppendWhitelistMessages( - txValidity, - targetAddress - ); + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteAppendWhitelistMessage(adminNode.wallet.address, targetAddress, txValidity) + ); } export async function buildBanWriterPayload(context, readerAddress) { const adminNode = context.adminBootstrap; const txValidity = await deriveIndexerSequenceState(adminNode.base); - return new CompleteStateMessageOperations(adminNode.wallet, config) - .assembleBanWriterMessage( - readerAddress, - txValidity - ); + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteBanWriterMessage(adminNode.wallet.address, readerAddress, txValidity) + ); } export function mutateAppendWhitelistPayloadForInvalidSchema(t, validPayload) { diff --git a/tests/unit/state/apply/balanceInitialization/balanceInitializationScenarioHelpers.js b/tests/unit/state/apply/balanceInitialization/balanceInitializationScenarioHelpers.js index 4035d9ba..bda5b9ca 100644 --- a/tests/unit/state/apply/balanceInitialization/balanceInitializationScenarioHelpers.js +++ b/tests/unit/state/apply/balanceInitialization/balanceInitializationScenarioHelpers.js @@ -6,7 +6,7 @@ import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; import { AUTOBASE_VALUE_ENCODING } from '../../../../../src/utils/constants.js'; import { toTerm } from '../../../../../src/core/state/utils/balance.js'; import { safeDecodeApplyOperation, safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; @@ -42,11 +42,14 @@ async function bootstrapAdmin(context) { export async function buildBalanceInitializationPayload(context, recipientAddress, balanceBuffer) { const adminNode = context.adminBootstrap; const txValidity = await deriveIndexerSequenceState(adminNode.base); - const messages = await new CompleteStateMessageOperations(adminNode.wallet, config).assembleBalanceInitializationMessages( - txValidity, - [[recipientAddress, balanceBuffer]] - ); - return messages[0]; + const payload = await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteBalanceInitializationMessage( + adminNode.wallet.address, + recipientAddress, + balanceBuffer, + txValidity + ); + return safeEncodeApplyOperation(payload); } export async function buildBalanceInitializationPayloadWithTxValidity({ @@ -60,11 +63,14 @@ export async function buildBalanceInitializationPayloadWithTxValidity({ } const adminNode = context.adminBootstrap; - const messages = await new CompleteStateMessageOperations(adminNode.wallet, config).assembleBalanceInitializationMessages( - mutatedTxValidity, - [[decoded.bio.ia, decoded.bio.am]] - ); - return messages[0]; + const payload = await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteBalanceInitializationMessage( + adminNode.wallet.address, + decoded.bio.ia, + decoded.bio.am, + mutatedTxValidity + ); + return safeEncodeApplyOperation(payload); } export async function buildDefaultBalanceInitializationPayload(context) { diff --git a/tests/unit/state/apply/banValidator/banValidatorScenarioHelpers.js b/tests/unit/state/apply/banValidator/banValidatorScenarioHelpers.js index 256a5ab0..adf90ee0 100644 --- a/tests/unit/state/apply/banValidator/banValidatorScenarioHelpers.js +++ b/tests/unit/state/apply/banValidator/banValidatorScenarioHelpers.js @@ -1,7 +1,7 @@ import b4a from 'b4a'; import PeerWallet from 'trac-wallet'; import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; import nodeEntryUtils, { ZERO_LICENSE } from '../../../../../src/core/state/utils/nodeEntry.js'; import addressUtils from '../../../../../src/core/state/utils/address.js'; import lengthEntryUtils from '../../../../../src/core/state/utils/lengthEntry.js'; @@ -62,9 +62,9 @@ export async function buildBanValidatorPayload( /* cover tests */ ) { const txValidity = await deriveIndexerSequenceState(adminPeer.base); - return new CompleteStateMessageOperations(adminPeer.wallet, config).assembleBanWriterMessage( - validatorPeer.wallet.address, - txValidity + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminPeer.wallet, config) + .buildCompleteBanWriterMessage(adminPeer.wallet.address, validatorPeer.wallet.address, txValidity) ); } @@ -73,9 +73,9 @@ export async function buildBanValidatorPayloadWithTxValidity( mutatedTxValidity, { adminPeer = context.adminBootstrap, validatorPeer = selectWriterPeer(context) } = {} ) { - return new CompleteStateMessageOperations(adminPeer.wallet, config).assembleBanWriterMessage( - validatorPeer.wallet.address, - mutatedTxValidity + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminPeer.wallet, config) + .buildCompleteBanWriterMessage(adminPeer.wallet.address, validatorPeer.wallet.address, mutatedTxValidity) ); } @@ -339,11 +339,10 @@ export async function promoteValidatorToIndexer( { adminPeer = context.adminBootstrap, validatorPeer = selectWriterPeer(context) } = {} ) { const txValidity = await deriveIndexerSequenceState(adminPeer.base); - const payload = await new CompleteStateMessageOperations(adminPeer.wallet, config) - .assembleAddIndexerMessage( - validatorPeer.wallet.address, - txValidity - ); + const payload = safeEncodeApplyOperation( + await applyStateMessageFactory(adminPeer.wallet, config) + .buildCompleteAddIndexerMessage(adminPeer.wallet.address, validatorPeer.wallet.address, txValidity) + ); await adminPeer.base.append(payload); await adminPeer.base.update(); diff --git a/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentScenarioHelpers.js b/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentScenarioHelpers.js index 30f4f0b3..1925eb2c 100644 --- a/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentScenarioHelpers.js +++ b/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentScenarioHelpers.js @@ -1,6 +1,5 @@ import b4a from 'b4a'; -import PartialStateMessageOperations from '../../../../../src/messages/partialStateMessages/PartialStateMessageOperations.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; import { deriveIndexerSequenceState } from '../../../../helpers/autobaseTestHelpers.js'; import { setupAdminNetwork, @@ -109,15 +108,17 @@ export async function buildBootstrapDeploymentPayload(context, options = {}) { context.bootstrapDeployment?.txValidity ?? (await deriveIndexerSequenceState(validatorPeer.base)); - const partial = await new PartialStateMessageOperations(deployerPeer.wallet, config) - .assembleBootstrapDeploymentMessage( + const partial = await applyStateMessageFactory(deployerPeer.wallet, config) + .buildPartialBootstrapDeploymentMessage( + deployerPeer.wallet.address, externalBootstrap.toString('hex'), channel.toString('hex'), - txValidity.toString('hex') + txValidity.toString('hex'), + 'json' ); - return new CompleteStateMessageOperations(validatorPeer.wallet, config) - .assembleCompleteBootstrapDeployment( + const payload = await applyStateMessageFactory(validatorPeer.wallet, config) + .buildCompleteBootstrapDeploymentMessage( partial.address, b4a.from(partial.bdo.tx, 'hex'), b4a.from(partial.bdo.txv, 'hex'), @@ -126,6 +127,7 @@ export async function buildBootstrapDeploymentPayload(context, options = {}) { b4a.from(partial.bdo.in, 'hex'), b4a.from(partial.bdo.is, 'hex') ); + return safeEncodeApplyOperation(payload); } export async function buildBootstrapDeploymentPayloadWithTxValidity(context, txValidity, options = {}) { diff --git a/tests/unit/state/apply/common/commonScenarioHelper.js b/tests/unit/state/apply/common/commonScenarioHelper.js index f9ecdcbf..89b86de2 100644 --- a/tests/unit/state/apply/common/commonScenarioHelper.js +++ b/tests/unit/state/apply/common/commonScenarioHelper.js @@ -5,7 +5,8 @@ import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import { AUTOBASE_VALUE_ENCODING } from '../../../../../src/utils/constants.js'; import { buildAddAdminRequesterPayload } from '../addAdmin/addAdminScenarioHelpers.js'; import { config } from '../../../../helpers/config.js'; @@ -59,13 +60,16 @@ export async function initializeBalances(context, recipients) { if (!adminNode || !Array.isArray(recipients) || recipients.length === 0) return; const txValidity = await deriveIndexerSequenceState(adminNode.base); - const payloads = await new CompleteStateMessageOperations(adminNode.wallet, config).assembleBalanceInitializationMessages( - txValidity, - recipients - ); - - for (const payload of payloads) { - await adminNode.base.append(payload); + for (const [recipientAddress, balanceBuffer] of recipients) { + const payload = await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteBalanceInitializationMessage( + adminNode.wallet.address, + recipientAddress, + balanceBuffer, + txValidity + ); + const encoded = safeEncodeApplyOperation(payload); + await adminNode.base.append(encoded); await adminNode.base.update(); await eventFlush(); } @@ -76,12 +80,9 @@ export async function whitelistAddress(context, address) { if (!adminNode || !address) return; const txValidity = await deriveIndexerSequenceState(adminNode.base); - const payload = await new CompleteStateMessageOperations(adminNode.wallet, config).assembleAppendWhitelistMessages( - txValidity, - address - ); - - await adminNode.base.append(payload); + const payload = await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteAppendWhitelistMessage(adminNode.wallet.address, address, txValidity); + await adminNode.base.append(safeEncodeApplyOperation(payload)); await adminNode.base.update(); await eventFlush(); } diff --git a/tests/unit/state/apply/common/payload-structure/initializationDisabledScenario.js b/tests/unit/state/apply/common/payload-structure/initializationDisabledScenario.js index c8f65069..950d37cf 100644 --- a/tests/unit/state/apply/common/payload-structure/initializationDisabledScenario.js +++ b/tests/unit/state/apply/common/payload-structure/initializationDisabledScenario.js @@ -1,6 +1,7 @@ import { eventFlush, deriveIndexerSequenceState } from '../../../../../helpers/autobaseTestHelpers.js'; import OperationValidationScenarioBase from '../base/OperationValidationScenarioBase.js'; -import CompleteStateMessageOperations from '../../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../../../src/utils/protobuf/operationHelpers.js'; import { config } from '../../../../../helpers/config.js'; export default class InitializationDisabledScenario extends OperationValidationScenarioBase { @@ -34,9 +35,13 @@ async function disableInitializationAndApply(context, invalidPayload) { } const txValidity = await deriveIndexerSequenceState(adminNode.base); - const disablePayload = await new CompleteStateMessageOperations(adminNode.wallet, config).assembleDisableInitializationMessage( - adminNode.base.local.key, - txValidity + const disablePayload = safeEncodeApplyOperation( + await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteDisableInitializationMessage( + adminNode.wallet.address, + adminNode.base.local.key, + txValidity + ) ); await adminNode.base.append(disablePayload); diff --git a/tests/unit/state/apply/disableInitialization/disableInitializationScenarioHelpers.js b/tests/unit/state/apply/disableInitialization/disableInitializationScenarioHelpers.js index 49723d36..e384deca 100644 --- a/tests/unit/state/apply/disableInitialization/disableInitializationScenarioHelpers.js +++ b/tests/unit/state/apply/disableInitialization/disableInitializationScenarioHelpers.js @@ -6,7 +6,7 @@ import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; import { AUTOBASE_VALUE_ENCODING, EntryType } from '../../../../../src/utils/constants.js'; import { safeDecodeApplyOperation, safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import { safeWriteUInt32BE } from '../../../../../src/utils/buffer.js'; @@ -55,20 +55,26 @@ export async function buildDisableInitializationPayload(context) { const adminNode = context.adminBootstrap; const txValidity = await deriveIndexerSequenceState(adminNode.base); - return new CompleteStateMessageOperations(adminNode.wallet, config) - .assembleDisableInitializationMessage( - adminNode.base.local.key, - txValidity - ); + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteDisableInitializationMessage( + adminNode.wallet.address, + adminNode.base.local.key, + txValidity + ) + ); } export async function buildDisableInitializationPayloadWithTxValidity(context, txValidity) { const adminNode = context.adminBootstrap; - return new CompleteStateMessageOperations(adminNode.wallet, config) - .assembleDisableInitializationMessage( - adminNode.base.local.key, - txValidity - ); + return safeEncodeApplyOperation( + await applyStateMessageFactory(adminNode.wallet, config) + .buildCompleteDisableInitializationMessage( + adminNode.wallet.address, + adminNode.base.local.key, + txValidity + ) + ); } export async function assertInitializationDisabledState(t, base, payload) { diff --git a/tests/unit/state/apply/removeWriter/removeWriterScenarioHelpers.js b/tests/unit/state/apply/removeWriter/removeWriterScenarioHelpers.js index 23a822b4..07fe7380 100644 --- a/tests/unit/state/apply/removeWriter/removeWriterScenarioHelpers.js +++ b/tests/unit/state/apply/removeWriter/removeWriterScenarioHelpers.js @@ -7,8 +7,8 @@ import { assertValidatorReward, promotePeerToWriter } from '../addWriter/addWriterScenarioHelpers.js'; -import PartialStateMessageOperations from '../../../../../src/messages/partialStateMessages/PartialStateMessageOperations.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import nodeEntryUtils from '../../../../../src/core/state/utils/nodeEntry.js'; import addressUtils from '../../../../../src/core/state/utils/address.js'; import { toBalance, BALANCE_FEE, BALANCE_TO_STAKE } from '../../../../../src/core/state/utils/balance.js'; @@ -180,18 +180,23 @@ export async function buildRemoveWriterPayloadWithTxValidity(context, mutatedTxV } const { readerPeer = selectWriterPeer(context), validatorPeer = context.adminBootstrap, writerKeyBuffer = null } = options; const writerKey = writerKeyBuffer ?? readerPeer.base.local.key; - const partial = await new PartialStateMessageOperations(readerPeer.wallet, config).assembleRemoveWriterMessage( - writerKey.toString('hex'), - mutatedTxValidity.toString('hex') - ); - return new CompleteStateMessageOperations(validatorPeer.wallet, config).assembleRemoveWriterMessage( - partial.address, - b4a.from(partial.rao.tx, 'hex'), - mutatedTxValidity, - b4a.from(partial.rao.iw, 'hex'), - b4a.from(partial.rao.in, 'hex'), - b4a.from(partial.rao.is, 'hex') - ); + const partial = await applyStateMessageFactory(readerPeer.wallet, config) + .buildPartialRemoveWriterMessage( + readerPeer.wallet.address, + writerKey.toString('hex'), + mutatedTxValidity.toString('hex'), + 'json' + ); + const payload = await applyStateMessageFactory(validatorPeer.wallet, config) + .buildCompleteRemoveWriterMessage( + partial.address, + b4a.from(partial.rao.tx, 'hex'), + mutatedTxValidity, + b4a.from(partial.rao.iw, 'hex'), + b4a.from(partial.rao.in, 'hex'), + b4a.from(partial.rao.is, 'hex') + ); + return safeEncodeApplyOperation(payload); } export async function snapshotDowngradedWriterEntry(context) { diff --git a/tests/unit/state/apply/transfer/transferDoubleSpendAcrossValidatorsScenario.js b/tests/unit/state/apply/transfer/transferDoubleSpendAcrossValidatorsScenario.js index c4388002..e3aab677 100644 --- a/tests/unit/state/apply/transfer/transferDoubleSpendAcrossValidatorsScenario.js +++ b/tests/unit/state/apply/transfer/transferDoubleSpendAcrossValidatorsScenario.js @@ -1,7 +1,7 @@ import b4a from 'b4a'; import { test } from 'brittle'; -import PartialStateMessageOperations from '../../../../../src/messages/partialStateMessages/PartialStateMessageOperations.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; +import { safeEncodeApplyOperation } from '../../../../../src/utils/protobuf/operationHelpers.js'; import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; import { setupTransferScenario, @@ -50,40 +50,48 @@ export default function transferDoubleSpendAcrossValidatorsScenario() { const txValidityA = await deriveIndexerSequenceState(primaryValidator.base); const txValidityB = await deriveIndexerSequenceState(secondaryValidator.base); - const partialA = await new PartialStateMessageOperations(senderPeer.wallet, config) - .assembleTransferOperationMessage( + const partialA = await applyStateMessageFactory(senderPeer.wallet, config) + .buildPartialTransferOperationMessage( + senderPeer.wallet.address, recipientA.wallet.address, b4a.toString(DEFAULT_TRANSFER_AMOUNT, 'hex'), - b4a.toString(txValidityA, 'hex') + b4a.toString(txValidityA, 'hex'), + 'json' ); - const partialB = await new PartialStateMessageOperations(senderPeer.wallet, config) - .assembleTransferOperationMessage( + const partialB = await applyStateMessageFactory(senderPeer.wallet, config) + .buildPartialTransferOperationMessage( + senderPeer.wallet.address, recipientB.wallet.address, b4a.toString(DEFAULT_TRANSFER_AMOUNT, 'hex'), - b4a.toString(txValidityB, 'hex') + b4a.toString(txValidityB, 'hex'), + 'json' ); - const payloadA = await new CompleteStateMessageOperations(primaryValidator.wallet, config) - .assembleCompleteTransferOperationMessage( - partialA.address, - b4a.from(partialA.tro.tx, 'hex'), - b4a.from(partialA.tro.txv, 'hex'), - b4a.from(partialA.tro.in, 'hex'), - partialA.tro.to, - b4a.from(partialA.tro.am, 'hex'), - b4a.from(partialA.tro.is, 'hex') - ); - - const payloadB = await new CompleteStateMessageOperations(secondaryValidator.wallet, config) - .assembleCompleteTransferOperationMessage( - partialB.address, - b4a.from(partialB.tro.tx, 'hex'), - b4a.from(partialB.tro.txv, 'hex'), - b4a.from(partialB.tro.in, 'hex'), - partialB.tro.to, - b4a.from(partialB.tro.am, 'hex'), - b4a.from(partialB.tro.is, 'hex') - ); + const payloadA = safeEncodeApplyOperation( + await applyStateMessageFactory(primaryValidator.wallet, config) + .buildCompleteTransferOperationMessage( + partialA.address, + b4a.from(partialA.tro.tx, 'hex'), + b4a.from(partialA.tro.txv, 'hex'), + b4a.from(partialA.tro.in, 'hex'), + partialA.tro.to, + b4a.from(partialA.tro.am, 'hex'), + b4a.from(partialA.tro.is, 'hex') + ) + ); + + const payloadB = safeEncodeApplyOperation( + await applyStateMessageFactory(secondaryValidator.wallet, config) + .buildCompleteTransferOperationMessage( + partialB.address, + b4a.from(partialB.tro.tx, 'hex'), + b4a.from(partialB.tro.txv, 'hex'), + b4a.from(partialB.tro.in, 'hex'), + partialB.tro.to, + b4a.from(partialB.tro.am, 'hex'), + b4a.from(partialB.tro.is, 'hex') + ) + ); // Apply first transfer successfully via primary validator. await primaryValidator.base.append(payloadA); diff --git a/tests/unit/state/apply/transfer/transferScenarioHelpers.js b/tests/unit/state/apply/transfer/transferScenarioHelpers.js index de4f2499..c842cdbb 100644 --- a/tests/unit/state/apply/transfer/transferScenarioHelpers.js +++ b/tests/unit/state/apply/transfer/transferScenarioHelpers.js @@ -1,7 +1,6 @@ import b4a from 'b4a'; import PeerWallet from 'trac-wallet'; -import PartialStateMessageOperations from '../../../../../src/messages/partialStateMessages/PartialStateMessageOperations.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; import { deriveIndexerSequenceState, eventFlush } from '../../../../helpers/autobaseTestHelpers.js'; import { setupAdminNetwork, @@ -123,15 +122,17 @@ export async function buildTransferPayload( const resolvedTxValidity = txValidity ?? (await deriveIndexerSequenceState(validatorPeer.base)); - const partial = await new PartialStateMessageOperations(senderPeer.wallet, config) - .assembleTransferOperationMessage( + const partial = await applyStateMessageFactory(senderPeer.wallet, config) + .buildPartialTransferOperationMessage( + senderPeer.wallet.address, recipientAddress, b4a.toString(amount, 'hex'), - b4a.toString(resolvedTxValidity, 'hex') + b4a.toString(resolvedTxValidity, 'hex'), + 'json' ); - return new CompleteStateMessageOperations(validatorPeer.wallet, config) - .assembleCompleteTransferOperationMessage( + const payload = await applyStateMessageFactory(validatorPeer.wallet, config) + .buildCompleteTransferOperationMessage( partial.address, b4a.from(partial.tro.tx, 'hex'), b4a.from(partial.tro.txv, 'hex'), @@ -140,6 +141,7 @@ export async function buildTransferPayload( b4a.from(partial.tro.am, 'hex'), b4a.from(partial.tro.is, 'hex') ); + return safeEncodeApplyOperation(payload); } export async function buildTransferPayloadWithTxValidity( diff --git a/tests/unit/state/apply/txOperation/txOperationScenarioHelpers.js b/tests/unit/state/apply/txOperation/txOperationScenarioHelpers.js index 71c4eccf..8ac0e71b 100644 --- a/tests/unit/state/apply/txOperation/txOperationScenarioHelpers.js +++ b/tests/unit/state/apply/txOperation/txOperationScenarioHelpers.js @@ -1,6 +1,5 @@ import b4a from 'b4a'; -import PartialStateMessageOperations from '../../../../../src/messages/partialStateMessages/PartialStateMessageOperations.js'; -import CompleteStateMessageOperations from '../../../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'; +import { applyStateMessageFactory } from '../../../../../src/messages/state/applyStateMessageFactory.js'; import { deriveIndexerSequenceState, eventFlush @@ -139,20 +138,22 @@ export async function buildTxOperationPayload( writerKeyBuffer = broadcasterPeer.base.local.key } = {} ) { - const resolvedTxValidity = txValidity ?? (await deriveIndexerSequenceState(validatorPeer.base)); + const resolvedTxValidity = txValidity ?? (await deriveIndexerSequenceState(validatorPeer.base)); - const partial = await new PartialStateMessageOperations(broadcasterPeer.wallet, config) - .assembleTransactionOperationMessage( + const partial = await applyStateMessageFactory(broadcasterPeer.wallet, config) + .buildPartialTransactionOperationMessage( + broadcasterPeer.wallet.address, writerKeyBuffer.toString('hex'), resolvedTxValidity.toString('hex'), contentHash.toString('hex'), externalBootstrap.toString('hex'), - msbBootstrap.toString('hex') + msbBootstrap.toString('hex'), + 'json' ); - return new CompleteStateMessageOperations(validatorPeer.wallet, config) - .assembleCompleteTransactionOperationMessage( - partial.address, + const payload = await applyStateMessageFactory(validatorPeer.wallet, config) + .buildCompleteTransactionOperationMessage( + partial.address, b4a.from(partial.txo.tx, 'hex'), b4a.from(partial.txo.txv, 'hex'), b4a.from(partial.txo.iw, 'hex'), @@ -162,6 +163,7 @@ export async function buildTxOperationPayload( b4a.from(partial.txo.bs, 'hex'), b4a.from(partial.txo.mbs, 'hex') ); + return safeEncodeApplyOperation(payload); } export async function buildTxOperationPayloadWithTxValidity(context, txValidity, options = {}) { diff --git a/tests/unit/unit.test.js b/tests/unit/unit.test.js index 708baedc..624d3637 100644 --- a/tests/unit/unit.test.js +++ b/tests/unit/unit.test.js @@ -7,7 +7,7 @@ async function runTests() { await import('./network/networkModule.test.js') await import('./state/stateModule.test.js'); await import('./utils/utils.test.js'); - // await import('./messageOperations/stateMessageOperations.test.js'); // Broken test - needs fixing + await import('./messages/messages.test.js'); test.resume(); } diff --git a/tests/unit/utils/buffer/buffer.test.js b/tests/unit/utils/buffer/buffer.test.js index 489b94e7..6c27ac70 100644 --- a/tests/unit/utils/buffer/buffer.test.js +++ b/tests/unit/utils/buffer/buffer.test.js @@ -1,6 +1,7 @@ import test from 'brittle'; import b4a from 'b4a'; -import {createMessage, isBufferValid, safeWriteUInt32BE, deepCopyBuffer} from '../../../../src/utils/buffer.js'; +import { createMessage, isBufferValid, safeWriteUInt32BE, deepCopyBuffer, encodeCapabilities, timestampToBuffer, sessionIdToBuffer } from '../../../../src/utils/buffer.js'; +import { errorMessageIncludes } from "../../../helpers/regexHelper.js"; const invalidDataTypes = [ null, @@ -188,3 +189,61 @@ test('deepCopyBuffer - modifying copy does not affect original (is not a referen t.is(buf[0], 9, 'original unchanged'); t.is(copy[0], 1, 'copy modified independently'); }); + +test('encodeCapabilities - deterministic ordering and encoding', t => { + const caps = ['cap-b', 'cap-a']; + const result = encodeCapabilities(caps); + + const capA = b4a.from('cap-a', 'utf8'); + const capB = b4a.from('cap-b', 'utf8'); + + const expected = b4a.concat([ + b4a.from([0x00, capA.length]), + capA, + b4a.from([0x00, capB.length]), + capB + ]); + + t.is(result.length, expected.length, 'length matches'); + t.ok(b4a.equals(result, expected), 'ordering is sorted and encoding matches'); +}); + +test('encodeCapabilities - empty array yields empty buffer', t => { + const result = encodeCapabilities([]); + t.ok(b4a.isBuffer(result), 'returns a buffer'); + t.is(result.length, 0, 'empty buffer for no caps'); +}); + +test('encodeCapabilities - throws on invalid input', t => { + t.exception( + () => encodeCapabilities('not-array'), + errorMessageIncludes('Capabilities must be an array') + ); + + t.exception( + () => encodeCapabilities([1, 2]), + errorMessageIncludes('must contain only strings') + ); +}); + +test('timestampToBuffer and sessionIdToBuffer - encode uint64 BE', t => { + const ts = 2n ** 53n; // beyond uint32 + const sid = 1234567890123n; + + const tsBuf = timestampToBuffer(ts); + const sidBuf = sessionIdToBuffer(sid); + + t.is(tsBuf.length, 8); + t.is(sidBuf.length, 8); + t.is(tsBuf.readBigUInt64BE(0), ts); + t.is(sidBuf.readBigUInt64BE(0), sid); +}); + +test('timestampToBuffer and sessionIdToBuffer - reject invalid input', t => { + t.exception(() => timestampToBuffer(-1), errorMessageIncludes('timestamp')); + t.exception(() => timestampToBuffer(1.5), errorMessageIncludes('timestamp')); + t.exception(() => timestampToBuffer('1'), errorMessageIncludes('timestamp')); + t.exception(() => sessionIdToBuffer(-1), errorMessageIncludes('session id')); + t.exception(() => sessionIdToBuffer(1.1), errorMessageIncludes('session id')); + t.exception(() => sessionIdToBuffer('abc'), errorMessageIncludes('session id')); +}); diff --git a/tests/unit/utils/normalizers/normalizers.test.js b/tests/unit/utils/normalizers/normalizers.test.js new file mode 100644 index 00000000..ecbfdf15 --- /dev/null +++ b/tests/unit/utils/normalizers/normalizers.test.js @@ -0,0 +1,469 @@ +import { test } from 'brittle'; +import b4a from 'b4a'; + +import { config } from '../../../helpers/config.js'; +import { randomAddress } from '../../state/stateTestUtils.js'; +import { errorMessageIncludes } from '../../../helpers/regexHelper.js'; +import { OperationType } from '../../../../src/utils/constants.js'; +import { + normalizeBootstrapDeploymentOperation, + normalizeDecodedPayloadForJson, + normalizeRoleAccessOperation, + normalizeTransactionOperation, + normalizeTransferOperation +} from '../../../../src/utils/normalizers.js'; +import { addressToBuffer } from '../../../../src/core/state/utils/address.js'; +import { bigIntTo16ByteBuffer } from '../../../../src/utils/amountSerialization.js'; + +const hex = (value, bytes) => value.repeat(bytes); +const toBuffer = value => b4a.from(value, 'hex'); + +test('normalizeTransferOperation normalizes hex strings and addresses', t => { + const sender = randomAddress(config.addressPrefix); + const recipient = randomAddress(config.addressPrefix); + const tx = hex('11', 32); + const txv = hex('22', 32); + const nonce = hex('33', 32); + const amount = hex('44', 16); + const signature = hex('55', 64); + + const payload = { + type: OperationType.TRANSFER, + address: sender, + tro: { + tx, + txv, + in: nonce, + to: recipient, + am: amount, + is: signature + } + }; + + const normalized = normalizeTransferOperation(payload, config); + + t.is(normalized.type, OperationType.TRANSFER); + t.ok(b4a.equals(normalized.address, addressToBuffer(sender, config.addressPrefix))); + t.ok(b4a.equals(normalized.tro.tx, toBuffer(tx))); + t.ok(b4a.equals(normalized.tro.txv, toBuffer(txv))); + t.ok(b4a.equals(normalized.tro.in, toBuffer(nonce))); + t.ok(b4a.equals(normalized.tro.to, addressToBuffer(recipient, config.addressPrefix))); + t.ok(b4a.equals(normalized.tro.am, toBuffer(amount))); + t.ok(b4a.equals(normalized.tro.is, toBuffer(signature))); +}); + +test('normalizeTransferOperation accepts buffer inputs', t => { + const sender = randomAddress(config.addressPrefix); + const recipient = randomAddress(config.addressPrefix); + const tx = toBuffer(hex('aa', 32)); + const txv = toBuffer(hex('bb', 32)); + const nonce = toBuffer(hex('cc', 32)); + const amount = toBuffer(hex('dd', 16)); + const signature = toBuffer(hex('ee', 64)); + + const payload = { + type: OperationType.TRANSFER, + address: sender, + tro: { + tx, + txv, + in: nonce, + to: recipient, + am: amount, + is: signature + } + }; + + const normalized = normalizeTransferOperation(payload, config); + t.ok(b4a.equals(normalized.tro.tx, tx)); + t.ok(b4a.equals(normalized.tro.txv, txv)); + t.ok(b4a.equals(normalized.tro.in, nonce)); + t.ok(b4a.equals(normalized.tro.am, amount)); + t.ok(b4a.equals(normalized.tro.is, signature)); +}); + +test('normalizeTransferOperation throws on missing payload fields', t => { + t.exception( + () => normalizeTransferOperation({ type: OperationType.TRANSFER }, config), + errorMessageIncludes('Invalid payload for transfer operation normalization.') + ); + + t.exception( + () => normalizeTransferOperation({ type: OperationType.TX, address: 'x', tro: {} }, config), + errorMessageIncludes('Missing required fields in transfer operation payload.') + ); + + const sender = randomAddress(config.addressPrefix); + const payload = { + type: OperationType.TRANSFER, + address: sender, + tro: { + tx: hex('11', 32), + txv: hex('22', 32), + in: hex('33', 32), + to: randomAddress(config.addressPrefix), + am: hex('44', 16) + } + }; + t.exception( + () => normalizeTransferOperation(payload, config), + errorMessageIncludes('Missing required fields in transfer operation payload.') + ); +}); + +test('normalizeTransferOperation throws on invalid hex string', t => { + const sender = randomAddress(config.addressPrefix); + const payload = { + type: OperationType.TRANSFER, + address: sender, + tro: { + tx: 'zz', + txv: hex('22', 32), + in: hex('33', 32), + to: randomAddress(config.addressPrefix), + am: hex('44', 16), + is: hex('55', 64) + } + }; + t.exception( + () => normalizeTransferOperation(payload, config), + errorMessageIncludes('Invalid hex string') + ); +}); + +test('normalizeTransactionOperation normalizes hex strings and addresses', t => { + const sender = randomAddress(config.addressPrefix); + const tx = hex('11', 32); + const txv = hex('22', 32); + const writerKey = hex('33', 32); + const contentHash = hex('44', 32); + const bootstrap = hex('55', 32); + const msbBootstrap = hex('66', 32); + const nonce = hex('77', 32); + const signature = hex('88', 64); + + const payload = { + type: OperationType.TX, + address: sender, + txo: { + tx, + txv, + iw: writerKey, + ch: contentHash, + bs: bootstrap, + mbs: msbBootstrap, + in: nonce, + is: signature + } + }; + + const normalized = normalizeTransactionOperation(payload, config); + t.is(normalized.type, OperationType.TX); + t.ok(b4a.equals(normalized.address, addressToBuffer(sender, config.addressPrefix))); + t.ok(b4a.equals(normalized.txo.tx, toBuffer(tx))); + t.ok(b4a.equals(normalized.txo.txv, toBuffer(txv))); + t.ok(b4a.equals(normalized.txo.iw, toBuffer(writerKey))); + t.ok(b4a.equals(normalized.txo.ch, toBuffer(contentHash))); + t.ok(b4a.equals(normalized.txo.bs, toBuffer(bootstrap))); + t.ok(b4a.equals(normalized.txo.mbs, toBuffer(msbBootstrap))); + t.ok(b4a.equals(normalized.txo.in, toBuffer(nonce))); + t.ok(b4a.equals(normalized.txo.is, toBuffer(signature))); +}); + +test('normalizeTransactionOperation accepts buffer inputs', t => { + const sender = randomAddress(config.addressPrefix); + const tx = toBuffer(hex('aa', 32)); + const txv = toBuffer(hex('bb', 32)); + const writerKey = toBuffer(hex('cc', 32)); + const contentHash = toBuffer(hex('dd', 32)); + const bootstrap = toBuffer(hex('ee', 32)); + const msbBootstrap = toBuffer(hex('ff', 32)); + const nonce = toBuffer(hex('12', 32)); + const signature = toBuffer(hex('34', 64)); + + const payload = { + type: OperationType.TX, + address: sender, + txo: { + tx, + txv, + iw: writerKey, + ch: contentHash, + bs: bootstrap, + mbs: msbBootstrap, + in: nonce, + is: signature + } + }; + + const normalized = normalizeTransactionOperation(payload, config); + t.ok(b4a.equals(normalized.txo.tx, tx)); + t.ok(b4a.equals(normalized.txo.txv, txv)); + t.ok(b4a.equals(normalized.txo.iw, writerKey)); + t.ok(b4a.equals(normalized.txo.ch, contentHash)); + t.ok(b4a.equals(normalized.txo.bs, bootstrap)); + t.ok(b4a.equals(normalized.txo.mbs, msbBootstrap)); + t.ok(b4a.equals(normalized.txo.in, nonce)); + t.ok(b4a.equals(normalized.txo.is, signature)); +}); + +test('normalizeTransactionOperation throws on missing payload fields', t => { + t.exception( + () => normalizeTransactionOperation({ type: OperationType.TX }, config), + errorMessageIncludes('Invalid payload for transaction operation normalization.') + ); + + t.exception( + () => normalizeTransactionOperation({ type: OperationType.TRANSFER, address: 'x', txo: {} }, config), + errorMessageIncludes('Missing required fields in transaction operation payload.') + ); + + const sender = randomAddress(config.addressPrefix); + const payload = { + type: OperationType.TX, + address: sender, + txo: { + tx: hex('11', 32), + txv: hex('22', 32), + iw: hex('33', 32), + ch: hex('44', 32), + bs: hex('55', 32), + mbs: hex('66', 32), + in: hex('77', 32) + } + }; + t.exception( + () => normalizeTransactionOperation(payload, config), + errorMessageIncludes('Missing required fields in transaction operation payload.') + ); +}); + +test('normalizeTransactionOperation throws on invalid hex string', t => { + const sender = randomAddress(config.addressPrefix); + const payload = { + type: OperationType.TX, + address: sender, + txo: { + tx: 'zz', + txv: hex('22', 32), + iw: hex('33', 32), + ch: hex('44', 32), + bs: hex('55', 32), + mbs: hex('66', 32), + in: hex('77', 32), + is: hex('88', 64) + } + }; + t.exception( + () => normalizeTransactionOperation(payload, config), + errorMessageIncludes('Invalid hex string') + ); +}); + +test('normalizeRoleAccessOperation normalizes hex strings and addresses', t => { + const sender = randomAddress(config.addressPrefix); + const payload = { + type: OperationType.ADD_WRITER, + address: sender, + rao: { + tx: hex('11', 32), + txv: hex('22', 32), + iw: hex('33', 32), + in: hex('44', 32), + is: hex('55', 64) + } + }; + + const normalized = normalizeRoleAccessOperation(payload, config); + t.is(normalized.type, OperationType.ADD_WRITER); + t.ok(b4a.equals(normalized.address, addressToBuffer(sender, config.addressPrefix))); + t.ok(b4a.equals(normalized.rao.tx, toBuffer(hex('11', 32)))); + t.ok(b4a.equals(normalized.rao.txv, toBuffer(hex('22', 32)))); + t.ok(b4a.equals(normalized.rao.iw, toBuffer(hex('33', 32)))); + t.ok(b4a.equals(normalized.rao.in, toBuffer(hex('44', 32)))); + t.ok(b4a.equals(normalized.rao.is, toBuffer(hex('55', 64)))); +}); + +test('normalizeRoleAccessOperation accepts buffer inputs', t => { + const sender = randomAddress(config.addressPrefix); + const tx = toBuffer(hex('aa', 32)); + const txv = toBuffer(hex('bb', 32)); + const writerKey = toBuffer(hex('cc', 32)); + const nonce = toBuffer(hex('dd', 32)); + const signature = toBuffer(hex('ee', 64)); + + const payload = { + type: OperationType.REMOVE_WRITER, + address: sender, + rao: { + tx, + txv, + iw: writerKey, + in: nonce, + is: signature + } + }; + + const normalized = normalizeRoleAccessOperation(payload, config); + t.ok(b4a.equals(normalized.rao.tx, tx)); + t.ok(b4a.equals(normalized.rao.txv, txv)); + t.ok(b4a.equals(normalized.rao.iw, writerKey)); + t.ok(b4a.equals(normalized.rao.in, nonce)); + t.ok(b4a.equals(normalized.rao.is, signature)); +}); + +test('normalizeRoleAccessOperation throws on missing payload fields', t => { + t.exception( + () => normalizeRoleAccessOperation({ type: OperationType.ADD_WRITER }, config), + errorMessageIncludes('Invalid payload for role access normalization.') + ); + + t.exception( + () => normalizeRoleAccessOperation({ type: OperationType.ADD_WRITER, address: 'x', rao: {} }, config), + errorMessageIncludes('Missing required fields in role access payload.') + ); +}); + +test('normalizeRoleAccessOperation throws on invalid hex string', t => { + const sender = randomAddress(config.addressPrefix); + const payload = { + type: OperationType.ADD_WRITER, + address: sender, + rao: { + tx: 'zz', + txv: hex('22', 32), + iw: hex('33', 32), + in: hex('44', 32), + is: hex('55', 64) + } + }; + t.exception( + () => normalizeRoleAccessOperation(payload, config), + errorMessageIncludes('Invalid hex string') + ); +}); + +test('normalizeBootstrapDeploymentOperation normalizes hex strings and addresses', t => { + const sender = randomAddress(config.addressPrefix); + const payload = { + type: OperationType.BOOTSTRAP_DEPLOYMENT, + address: sender, + bdo: { + tx: hex('11', 32), + txv: hex('22', 32), + bs: hex('33', 32), + ic: hex('44', 32), + in: hex('55', 32), + is: hex('66', 64) + } + }; + + const normalized = normalizeBootstrapDeploymentOperation(payload, config); + t.is(normalized.type, OperationType.BOOTSTRAP_DEPLOYMENT); + t.ok(b4a.equals(normalized.address, addressToBuffer(sender, config.addressPrefix))); + t.ok(b4a.equals(normalized.bdo.tx, toBuffer(hex('11', 32)))); + t.ok(b4a.equals(normalized.bdo.txv, toBuffer(hex('22', 32)))); + t.ok(b4a.equals(normalized.bdo.bs, toBuffer(hex('33', 32)))); + t.ok(b4a.equals(normalized.bdo.ic, toBuffer(hex('44', 32)))); + t.ok(b4a.equals(normalized.bdo.in, toBuffer(hex('55', 32)))); + t.ok(b4a.equals(normalized.bdo.is, toBuffer(hex('66', 64)))); +}); + +test('normalizeBootstrapDeploymentOperation accepts buffer inputs', t => { + const sender = randomAddress(config.addressPrefix); + const tx = toBuffer(hex('aa', 32)); + const txv = toBuffer(hex('bb', 32)); + const bootstrap = toBuffer(hex('cc', 32)); + const channel = toBuffer(hex('dd', 32)); + const nonce = toBuffer(hex('ee', 32)); + const signature = toBuffer(hex('ff', 64)); + + const payload = { + type: OperationType.BOOTSTRAP_DEPLOYMENT, + address: sender, + bdo: { + tx, + txv, + bs: bootstrap, + ic: channel, + in: nonce, + is: signature + } + }; + + const normalized = normalizeBootstrapDeploymentOperation(payload, config); + t.ok(b4a.equals(normalized.bdo.tx, tx)); + t.ok(b4a.equals(normalized.bdo.txv, txv)); + t.ok(b4a.equals(normalized.bdo.bs, bootstrap)); + t.ok(b4a.equals(normalized.bdo.ic, channel)); + t.ok(b4a.equals(normalized.bdo.in, nonce)); + t.ok(b4a.equals(normalized.bdo.is, signature)); +}); + +test('normalizeBootstrapDeploymentOperation throws on missing payload fields', t => { + t.exception( + () => normalizeBootstrapDeploymentOperation({ type: OperationType.BOOTSTRAP_DEPLOYMENT }, config), + errorMessageIncludes('Invalid payload for bootstrap deployment normalization.') + ); + + t.exception( + () => normalizeBootstrapDeploymentOperation({ type: OperationType.TX, address: 'x', bdo: {} }, config), + errorMessageIncludes('Missing required fields in bootstrap deployment payload.') + ); +}); + +test('normalizeBootstrapDeploymentOperation throws on invalid hex string', t => { + const sender = randomAddress(config.addressPrefix); + const payload = { + type: OperationType.BOOTSTRAP_DEPLOYMENT, + address: sender, + bdo: { + tx: 'zz', + txv: hex('22', 32), + bs: hex('33', 32), + ic: hex('44', 32), + in: hex('55', 32), + is: hex('66', 64) + } + }; + t.exception( + () => normalizeBootstrapDeploymentOperation(payload, config), + errorMessageIncludes('Invalid hex string') + ); +}); + +test('normalizeDecodedPayloadForJson converts buffers to strings', t => { + const address = randomAddress(config.addressPrefix); + const addressBuf = addressToBuffer(address, config.addressPrefix); + const amountBuf = bigIntTo16ByteBuffer(1234n); + const otherBuf = b4a.from('abcd', 'hex'); + + const payload = { + address: addressBuf, + am: amountBuf, + nested: { + to: addressBuf, + amount: amountBuf, + other: otherBuf + } + }; + + const normalized = normalizeDecodedPayloadForJson(payload, config); + t.is(normalized.address, address); + t.is(normalized.am, '1234'); + t.is(normalized.nested.to, address); + t.is(normalized.nested.amount, '1234'); + t.is(normalized.nested.other, 'abcd'); +}); + +test('normalizeDecodedPayloadForJson falls back to hex for invalid address buffers', t => { + const bad = b4a.from('deadbeef', 'hex'); + const payload = { address: bad }; + const normalized = normalizeDecodedPayloadForJson(payload, config); + t.is(normalized.address, b4a.toString(bad, 'hex')); +}); + +test('normalizeDecodedPayloadForJson returns input for non-objects', t => { + t.is(normalizeDecodedPayloadForJson(null, config), null); + t.is(normalizeDecodedPayloadForJson('value', config), 'value'); +}); diff --git a/tests/unit/utils/protobuf/operationHelpers.test.js b/tests/unit/utils/protobuf/operationHelpers.test.js index 1d6164cc..42ad79ce 100644 --- a/tests/unit/utils/protobuf/operationHelpers.test.js +++ b/tests/unit/utils/protobuf/operationHelpers.test.js @@ -2,12 +2,20 @@ import test from 'brittle'; import b4a from 'b4a'; import applyOperations from '../../../../src/utils/protobuf/applyOperations.cjs'; +import { + decodeV1networkOperation, + encodeV1networkOperation, + normalizeIncomingMessage, + safeDecodeApplyOperation, + safeEncodeApplyOperation, +} from '../../../../src/utils/protobuf/operationHelpers.js'; import fixtures from '../../../fixtures/protobuf.fixtures.js'; +import networkV1Fixtures from '../../../fixtures/networkV1.fixtures.js'; -//TODO add missing operations tests and fill fixtures with them test('Happy path encode/decode roundtrip for protobuf applyOperation payloads', t => { const payloadsHashMap = new Map([ ["txComplete", fixtures.validTransactionOperation], + ["txPartial", fixtures.validPartialTransactionOperation], ["addIndexer", fixtures.validAddIndexer], ["removeIndexer", fixtures.validRemoveIndexer], ["appendWhitelist", fixtures.validAppendWhitelist], @@ -19,8 +27,12 @@ test('Happy path encode/decode roundtrip for protobuf applyOperation payloads', ["removeWriterPartial", fixtures.validPartialRemoveWriter], ["adminRecoveryComplete", fixtures.validCompleteAdminRecovery], ["adminRecoveryPartial", fixtures.validPartialAdminRecovery], + ["bootstrapDeploymentComplete", fixtures.validCompleteBootstrapDeployment], + ["bootstrapDeploymentPartial", fixtures.validPartialBootstrapDeployment], ["transferComplete", fixtures.validTransferOperation], - ["balanceInitialization", fixtures.validBalanceInitOperation] + ["transferPartial", fixtures.validPartialTransferOperation], + ["balanceInitialization", fixtures.validBalanceInitOperation], + ["disableInitialization", fixtures.validDisableInitialization], ]); for (const [key, value] of payloadsHashMap) { @@ -101,6 +113,13 @@ test('Protobuf encode/decode is order-independent for all operation types', t => let decoded = applyOperations.Operation.decode(encoded); t.ok(JSON.stringify(decoded) === JSON.stringify(fixtures.validTransactionOperation), 'TX operation encodes/decodes correctly with shuffled fields'); + // Test TX operation (partial) + const shuffledPartialTxo = shuffleObject(fixtures.validPartialTransactionOperation.txo); + const shuffledPartialTx = { ...fixtures.validPartialTransactionOperation, txo: shuffledPartialTxo }; + encoded = applyOperations.Operation.encode(shuffledPartialTx); + decoded = applyOperations.Operation.decode(encoded); + t.ok(JSON.stringify(decoded) === JSON.stringify(fixtures.validPartialTransactionOperation), 'Partial TX operation encodes/decodes correctly with shuffled fields'); + // Test TRANSFER operation const shuffledTro = shuffleObject(fixtures.validTransferOperation.tro); const shuffledTransfer = { ...fixtures.validTransferOperation, tro: shuffledTro }; @@ -108,6 +127,13 @@ test('Protobuf encode/decode is order-independent for all operation types', t => decoded = applyOperations.Operation.decode(encoded); t.ok(JSON.stringify(decoded) === JSON.stringify(fixtures.validTransferOperation), 'TRANSFER operation encodes/decodes correctly with shuffled fields'); + // Test TRANSFER operation (partial) + const shuffledPartialTro = shuffleObject(fixtures.validPartialTransferOperation.tro); + const shuffledPartialTransfer = { ...fixtures.validPartialTransferOperation, tro: shuffledPartialTro }; + encoded = applyOperations.Operation.encode(shuffledPartialTransfer); + decoded = applyOperations.Operation.decode(encoded); + t.ok(JSON.stringify(decoded) === JSON.stringify(fixtures.validPartialTransferOperation), 'Partial TRANSFER operation encodes/decodes correctly with shuffled fields'); + // Test ADD_INDEXER operation const shuffledAco = shuffleObject(fixtures.validAddIndexer.aco); const shuffledAddIndexer = { ...fixtures.validAddIndexer, aco: shuffledAco }; @@ -184,4 +210,96 @@ test('Protobuf encode/decode is order-independent for all operation types', t => encoded = applyOperations.Operation.encode(shuffledPartialAdminRecovery); decoded = applyOperations.Operation.decode(encoded); t.ok(JSON.stringify(decoded) === JSON.stringify(fixtures.validPartialAdminRecovery), 'Partial ADMIN_RECOVERY operation encodes/decodes correctly with shuffled fields'); + + // Test BOOTSTRAP_DEPLOYMENT (complete) operation + const shuffledCompleteBootstrapBdo = shuffleObject(fixtures.validCompleteBootstrapDeployment.bdo); + const shuffledCompleteBootstrap = { ...fixtures.validCompleteBootstrapDeployment, bdo: shuffledCompleteBootstrapBdo }; + encoded = applyOperations.Operation.encode(shuffledCompleteBootstrap); + decoded = applyOperations.Operation.decode(encoded); + t.ok(JSON.stringify(decoded) === JSON.stringify(fixtures.validCompleteBootstrapDeployment), 'Complete BOOTSTRAP_DEPLOYMENT operation encodes/decodes correctly with shuffled fields'); + + // Test BOOTSTRAP_DEPLOYMENT (partial) operation + const shuffledPartialBootstrapBdo = shuffleObject(fixtures.validPartialBootstrapDeployment.bdo); + const shuffledPartialBootstrap = { ...fixtures.validPartialBootstrapDeployment, bdo: shuffledPartialBootstrapBdo }; + encoded = applyOperations.Operation.encode(shuffledPartialBootstrap); + decoded = applyOperations.Operation.decode(encoded); + t.ok(JSON.stringify(decoded) === JSON.stringify(fixtures.validPartialBootstrapDeployment), 'Partial BOOTSTRAP_DEPLOYMENT operation encodes/decodes correctly with shuffled fields'); + + // Test BALANCE_INITIALIZATION operation + const shuffledBio = shuffleObject(fixtures.validBalanceInitOperation.bio); + const shuffledBalanceInit = { ...fixtures.validBalanceInitOperation, bio: shuffledBio }; + encoded = applyOperations.Operation.encode(shuffledBalanceInit); + decoded = applyOperations.Operation.decode(encoded); + t.ok(JSON.stringify(decoded) === JSON.stringify(fixtures.validBalanceInitOperation), 'BALANCE_INITIALIZATION operation encodes/decodes correctly with shuffled fields'); + + // Test DISABLE_INITIALIZATION operation + const shuffledDisableCao = shuffleObject(fixtures.validDisableInitialization.cao); + const shuffledDisableInitialization = { ...fixtures.validDisableInitialization, cao: shuffledDisableCao }; + encoded = applyOperations.Operation.encode(shuffledDisableInitialization); + decoded = applyOperations.Operation.decode(encoded); + t.ok(JSON.stringify(decoded) === JSON.stringify(fixtures.validDisableInitialization), 'DISABLE_INITIALIZATION operation encodes/decodes correctly with shuffled fields'); +}); + +test('encodeV1networkOperation/decodeV1networkOperation roundtrip for network v1 payloads', t => { + const payloadsHashMap = new Map([ + ['validatorConnectionRequest', networkV1Fixtures.payloadValidatorConnectionRequest], + ['validatorConnectionResponse', networkV1Fixtures.payloadValidatorConnectionResponse], + ['livenessRequest', networkV1Fixtures.payloadLivenessRequest], + ['livenessResponse', networkV1Fixtures.payloadLivenessResponse], + ['broadcastTransactionRequest', networkV1Fixtures.payloadBroadcastTransactionRequest], + ['broadcastTransactionResponse', networkV1Fixtures.payloadBroadcastTransactionResponse], + ]); + + for (const [key, payload] of payloadsHashMap) { + const encoded = encodeV1networkOperation(payload); + const decoded = decodeV1networkOperation(encoded); + t.ok(b4a.isBuffer(encoded) && encoded.length > 0, `Payload ${key} encodes to a non-empty buffer`); + t.ok(JSON.stringify(decoded) === JSON.stringify(payload), `Payload ${key} decodes back correctly`); + } +}); + +test('safeEncodeApplyOperation returns an empty buffer on encode errors (and does not throw)', t => { + const originalLog = console.log; + console.log = () => {}; + try { + const encoded = safeEncodeApplyOperation(fixtures.invalidPayloadWithMultipleOneOfKeys); + t.ok(b4a.isBuffer(encoded)); + t.is(encoded.length, 0); + } finally { + console.log = originalLog; + } +}); + +test('safeEncodeApplyOperation encodes valid payloads to a non-empty buffer', t => { + const encoded = safeEncodeApplyOperation(fixtures.validTransactionOperation); + t.ok(b4a.isBuffer(encoded)); + t.ok(encoded.length > 0); +}); + +test('safeDecodeApplyOperation returns null for invalid input (and does not throw)', t => { + t.is(safeDecodeApplyOperation(null), null); + t.is(safeDecodeApplyOperation({}), null); + t.is(safeDecodeApplyOperation('not-a-buffer'), null); + + const originalLog = console.log; + console.log = () => {}; + try { + t.is(safeDecodeApplyOperation(b4a.from([0x0F])), null); + } finally { + console.log = originalLog; + } +}); + +test('normalizeIncomingMessage decodes buffers and JSON buffers', t => { + const payload = fixtures.validTransactionOperation; + const encoded = applyOperations.Operation.encode(payload); + + const decodedFromBuffer = normalizeIncomingMessage(encoded); + t.ok(JSON.stringify(decodedFromBuffer) === JSON.stringify(payload)); + + const decodedFromJsonBuffer = normalizeIncomingMessage({ type: 'Buffer', data: Array.from(encoded) }); + t.ok(JSON.stringify(decodedFromJsonBuffer) === JSON.stringify(payload)); + + t.is(normalizeIncomingMessage(null), null); + t.is(normalizeIncomingMessage({ type: 'nope', data: [] }), null); });