@massalabs/massa-web3
Version:
massa's web3 sdk client
193 lines • 7.79 kB
JavaScript
import * as unsigned from '../utils/big-varint';
import { Address } from '../basicElements/address';
import varint from 'varint';
import { U64 } from '../basicElements/serializers';
import { OperationType, } from './types';
export const PERIOD_TO_LIVE_DEFAULT = 9;
export const PERIOD_TO_LIVE_MAX = 9;
export const PERIOD_TO_LIVE_MIN = 1;
/*
* A class regrouping operation functions.
*/
export class OperationManager {
privateKey;
blockchainClient;
constructor(privateKey, blockchainClient) {
this.privateKey = privateKey;
this.blockchainClient = blockchainClient;
}
/**
* Serializes an operation according to the Massa protocol.
*
* @param operation - The operation to serialize.
*
* @returns A byte array representing the serialized operation.
*/
static serialize(operation) {
const components = [
unsigned.encode(operation.fee),
varint.encode(operation.expirePeriod),
varint.encode(operation.type),
];
switch (operation.type) {
case OperationType.Transaction:
operation = operation;
components.push(operation.recipientAddress.toBytes());
components.push(unsigned.encode(operation.amount));
break;
case OperationType.RollBuy:
case OperationType.RollSell:
operation = operation;
components.push(unsigned.encode(operation.amount));
break;
case OperationType.CallSmartContractFunction:
// @see https://docs.massa.net/docs/learn/operation-format-execution#callsc-operation-payload
operation = operation;
components.push(unsigned.encode(operation.maxGas));
components.push(unsigned.encode(operation.coins));
components.push(Address.fromString(operation.address).toBytes());
components.push(varint.encode(operation.func.length));
components.push(new TextEncoder().encode(operation.func));
components.push(varint.encode(operation.parameter.length));
components.push(operation.parameter);
break;
case OperationType.ExecuteSmartContractBytecode:
operation = operation;
components.push(unsigned.encode(operation.maxGas));
components.push(unsigned.encode(operation.maxCoins));
components.push(unsigned.encode(U64.fromNumber(operation.contractDataBinary.length)));
components.push(operation.contractDataBinary);
operation.datastore =
operation.datastore || new Map();
components.push(unsigned.encode(U64.fromNumber(operation.datastore.size)));
// length prefixed key-value pairs encoding
for (const [key, value] of operation.datastore) {
const keyLen = unsigned.encode(U64.fromNumber(key.length));
const valueLen = unsigned.encode(U64.fromNumber(value.length));
components.push(keyLen, key, valueLen, value);
}
break;
default:
throw new Error('Operation type not supported');
}
return new Uint8Array(components.reduce((acc, curr) => [...acc, ...curr], []));
}
/**
* Deserializes an operation according to the Massa protocol.
*
* @param data - The byte array to deserialize.
*
* @returns An new instance of OperationDetails representing the deserialized operation.
*/
static deserialize(data) {
let offset = 0;
// eslint-disable-next-line func-style
const nextVarInt = () => {
const value = unsigned.decode(data, offset);
offset += unsigned.encodingLength(value);
return value;
};
const operationDetails = {
fee: nextVarInt(),
expirePeriod: Number(nextVarInt()),
type: Number(nextVarInt()),
};
switch (operationDetails.type) {
case OperationType.Transaction: {
const { data: addrBytes, length } = Address.extractFromBuffer(data, offset);
const recipientAddress = Address.fromBytes(addrBytes);
offset += length;
return {
...operationDetails,
recipientAddress,
amount: nextVarInt(),
};
}
case OperationType.RollBuy:
case OperationType.RollSell: {
return {
...operationDetails,
amount: nextVarInt(),
};
}
default:
throw new Error('Operation type not supported');
}
}
/**
* Formats an operation for signing.
*
* @param chainId - The identifier of the blockchain network.
* @param operation - The operation to sign.
* @param key - The public key to sign the operation with.
*
* @returns The formatted operation ready for signing.
*/
static canonicalize(chainId, operation, key) {
// u64ToBytes is little endian
const networkId = new Uint8Array(U64.SIZE_BYTE);
const view = new DataView(networkId.buffer);
view.setBigUint64(0, chainId, false);
const data = OperationManager.serialize(operation);
const publicKeyBytes = key.toBytes();
return Uint8Array.from([...networkId, ...publicKeyBytes, ...data]);
}
/**
* Signs an operation for a given network.
*
* @remarks
* The chainId is used to counter replay attacks on a different chain.
*
* @param chainId - The identifier of the blockchain network.
* @param operation - The operation to sign.
*
* @returns A signature of the operation.
*/
async sign(chainId, operation) {
return this.privateKey.sign(OperationManager.canonicalize(chainId, operation, await this.privateKey.getPublicKey()));
}
/**
* Sends an operation to the blockchain.
*
* @param operation - The operation to send.
*
* @returns An operation Id.
*/
async send(operation) {
if (!this.blockchainClient) {
throw new Error('blockchainClient is mandatory to send operations');
}
const chainId = await this.blockchainClient.getChainId();
const signature = await this.sign(chainId, operation);
const data = OperationManager.serialize(operation);
const publicKey = await this.privateKey.getPublicKey();
return this.blockchainClient.sendOperation({
data,
publicKey: publicKey.toString(),
signature: signature.toString(),
});
}
}
/**
* Check the expire period validity.
*
* @remarks
* If the periodToLive is too big, the node will silently reject the operation.
* This is why the periodToLive is limited to an upper value.
*
* @param periodToLive - The period to live.
*
* @returns The expire period.
* @throws An error if the periodToLive is too low or too big.
*/
export function checkPeriodToLive(periodToLive = PERIOD_TO_LIVE_DEFAULT) {
if (periodToLive < PERIOD_TO_LIVE_MIN || periodToLive > PERIOD_TO_LIVE_MAX) {
throw new Error(`periodToLive must be between ${PERIOD_TO_LIVE_MIN} and ${PERIOD_TO_LIVE_MAX}.`);
}
}
export async function getAbsoluteExpirePeriod(client, periodToLive = PERIOD_TO_LIVE_DEFAULT) {
checkPeriodToLive(periodToLive);
const currentPeriod = await client.fetchPeriod();
return currentPeriod + periodToLive;
}
//# sourceMappingURL=operationManager.js.map