@blooo/hw-app-concordium
Version:
Ledger Hardware Wallet Concordium Application API
264 lines (244 loc) • 11 kB
text/typescript
import { AccountAddress, AccountTransactionType, getAccountTransactionHandler } from "@concordium/web-sdk";
/**
* Checks if a transaction handler exists for a given transaction kind.
* @param transactionKind The type of account transaction.
* @returns True if a handler exists, false otherwise.
*/
export function isAccountTransactionHandlerExists(transactionKind: AccountTransactionType) {
switch (transactionKind) {
case AccountTransactionType.Transfer:
return true;
case AccountTransactionType.TransferWithMemo:
return true;
case AccountTransactionType.DeployModule:
return true;
case AccountTransactionType.InitContract:
return true;
case AccountTransactionType.Update:
return true;
case AccountTransactionType.UpdateCredentials:
return true;
case AccountTransactionType.RegisterData:
return true;
case AccountTransactionType.ConfigureDelegation:
return true;
case AccountTransactionType.ConfigureBaker:
return true;
default:
return false;
}
}
/**
* Encodes a 64-bit unsigned integer to a Buffer using big endian.
* @param value A 64-bit integer.
* @param useLittleEndian A boolean value, if not given, the value is serialized in big endian.
* @returns Big endian serialization of the input.
*/
export function encodeWord64(value, useLittleEndian = false) {
if (value > BigInt(18446744073709551615) || value < BigInt(0)) {
throw new Error('The input has to be a 64 bit unsigned integer but it was: ' + value);
}
const arr = new ArrayBuffer(8);
const view = new DataView(arr);
view.setBigUint64(0, value, useLittleEndian);
return Buffer.from(new Uint8Array(arr));
}
/**
* Encodes a 32-bit signed integer to a Buffer using big endian.
* @param value A 32-bit integer.
* @param useLittleEndian A boolean value, if not given, the value is serialized in big endian.
* @returns Big endian serialization of the input.
*/
export function encodeInt32(value, useLittleEndian = false) {
if (value < -2147483648 || value > 2147483647 || !Number.isInteger(value)) {
throw new Error('The input has to be a 32 bit signed integer but it was: ' + value);
}
const arr = new ArrayBuffer(4);
const view = new DataView(arr);
view.setInt32(0, value, useLittleEndian);
return Buffer.from(new Int8Array(arr));
}
/**
* Encodes a 32-bit unsigned integer to a Buffer.
* @param value A 32-bit integer.
* @param useLittleEndian A boolean value, if not given, the value is serialized in big endian.
* @returns Big endian serialization of the input.
*/
export function encodeWord32(value, useLittleEndian = false) {
if (value > 4294967295 || value < 0 || !Number.isInteger(value)) {
throw new Error('The input has to be a 32 bit unsigned integer but it was: ' + value);
}
const arr = new ArrayBuffer(4);
const view = new DataView(arr);
view.setUint32(0, value, useLittleEndian);
return Buffer.from(new Uint8Array(arr));
}
/**
* Encodes a 16-bit unsigned integer to a Buffer using big endian.
* @param value A 16-bit integer.
* @param useLittleEndian A boolean value, if not given, the value is serialized in big endian.
* @returns Big endian serialization of the input.
*/
export function encodeWord16(value, useLittleEndian = false) {
if (value > 65535 || value < 0 || !Number.isInteger(value)) {
throw new Error('The input has to be a 16 bit unsigned integer but it was: ' + value);
}
const arr = new ArrayBuffer(2);
const view = new DataView(arr);
view.setUint16(0, value, useLittleEndian);
return Buffer.from(new Uint8Array(arr));
}
/**
* Encodes an 8-bit signed integer to a Buffer using big endian.
* @param value An 8-bit integer.
* @returns Big endian serialization of the input.
*/
export function encodeInt8(value: number): Buffer {
if (value > 127 || value < -128 || !Number.isInteger(value)) {
throw new Error('The input has to be a 8 bit signed integer but it was: ' + value);
}
return Buffer.from(Buffer.of(value));
}
/**
* Encodes a data blob (DataBlob or string) with its length as a prefix.
* @param blob The data blob to encode. Should be either a DataBlob instance (with .data: Buffer)
* or a string (UTF-8, or hex if prefixed as such).
* @returns A Buffer containing the length-prefixed data blob.
*/
export function encodeDataBlob(blob: any) {
let dataBuffer: Buffer;
if (typeof blob === "string") {
// If hex string (with 0x) use Buffer.from(str, 'hex')
if (blob.startsWith("0x") || blob.startsWith("0X")) {
dataBuffer = Buffer.from(blob.slice(2), "hex");
} else {
// Interpret string as UTF-8
dataBuffer = Buffer.from(blob, "utf-8");
}
} else if (blob && typeof blob === "object" && blob.data) {
// DataBlob type, .data may be Buffer or Uint8Array
// Ensure Buffer
dataBuffer = Buffer.isBuffer(blob.data) ? blob.data : Buffer.from(blob.data);
} else {
throw new Error("Invalid blob: must be a DataBlob or string");
}
const length = encodeWord16(dataBuffer.length);
return Buffer.concat([length, dataBuffer]);
}
/**
* Serializes a schedule payload.
* @param payload The schedule payload to serialize.
* @returns A Buffer containing the serialized schedule.
*/
function serializeSchedule(payload: any) {
const toAddressBuffer = AccountAddress.toBuffer(payload.toAddress);
const scheduleLength = encodeInt8(payload.schedule.length);
const bufferArray = payload.schedule.map((item: { timestamp: string, amount: string }) => {
const timestampBuffer = encodeWord64(item.timestamp);
const amountBuffer = encodeWord64(item.amount);
return Buffer.concat([timestampBuffer, amountBuffer]);
});
return Buffer.concat([toAddressBuffer, scheduleLength, ...bufferArray]);
}
/**
* Serializes a schedule and memo payload.
* @param payload The schedule and memo payload to serialize.
* @returns An object containing the serialized address and memo, and the schedule.
*/
function serializeScheduleAndMemo(payload: any) {
const toAddressBuffer = AccountAddress.toBuffer(payload.toAddress);
const scheduleLength = encodeInt8(payload.schedule.length);
const bufferArray = payload.schedule.map((item: { timestamp: string, amount: string }) => {
const timestampBuffer = encodeWord64(item.timestamp);
const amountBuffer = encodeWord64(item.amount);
return Buffer.concat([timestampBuffer, amountBuffer]);
});
const serializedMemo = encodeDataBlob(payload.memo);
return {
addressAndMemo: Buffer.concat([toAddressBuffer, serializedMemo]),
schedule: Buffer.concat([scheduleLength, ...bufferArray]),
};
}
/**
* Serializes a transfer with memo payload.
* @param payload The transfer with memo payload to serialize.
* @returns An object containing the serialized address and memo, and the amount.
*/
function serializeTransferWithMemo(payload: any) {
const serializedToAddress = AccountAddress.toBuffer(payload.toAddress);
const serializedMemo = encodeDataBlob(payload.memo);
const serializedAmount = encodeWord64(payload.amount.microCcdAmount);
return {
addressAndMemo: Buffer.concat([serializedToAddress, serializedMemo]),
amount: serializedAmount,
};
}
/**
* Serializes a transfer to public payload.
* @param payload The transfer to public payload to serialize.
* @returns A Buffer containing the serialized transfer to public data.
*/
function serializeTransferToPublic(payload: any) {
const remainingAmount = Buffer.from(payload.remainingAmount, 'hex');
const transferAmount = encodeWord64(payload.transferAmount.microCcdAmount);
const index = encodeWord64(payload.index);
const proofs = Buffer.from(payload.proofs, 'hex');
const proofsLength = encodeWord16(proofs.length);
return Buffer.concat([remainingAmount, transferAmount, index, proofsLength, proofs]);
}
/**
* Serializes an account transaction header.
* @param accountTransaction The account transaction header with metadata about the transaction.
* @param payloadSize The byte size of the serialized payload.
* @returns The serialized account transaction header.
*/
export const serializeAccountTransactionHeader = (accountTransaction, payloadSize) => {
const serializedSender = AccountAddress.toBuffer(accountTransaction.sender);
const serializedNonce = encodeWord64(accountTransaction.nonce);
const serializedEnergyAmount = encodeWord64(accountTransaction.energyAmount);
const serializedPayloadSize = encodeWord32(payloadSize);
const serializedExpiry = encodeWord64(accountTransaction.expiry);
return Buffer.concat([
serializedSender,
serializedNonce,
serializedEnergyAmount,
serializedPayloadSize,
serializedExpiry,
]);
}
/**
* Serializes a transaction and its signatures.
* This serialization when sha256 hashed is considered as the transaction hash,
* and is used to look up the status of a submitted transaction.
* @param accountTransaction The transaction to serialize.
* @param signatures Signatures on the signed digest of the transaction.
* @returns The serialization of the account transaction, which is used to calculate the transaction hash.
*/
export const serializeAccountTransaction = (accountTransaction) => {
let serializedType: Buffer;
let serializedPayload: Buffer;
serializedType = Buffer.from(Uint8Array.of(accountTransaction.transactionKind));
if (isAccountTransactionHandlerExists(accountTransaction.transactionKind) && accountTransaction.transactionKind !== AccountTransactionType.TransferWithMemo) {
const accountTransactionHandler = getAccountTransactionHandler(accountTransaction.transactionKind);
serializedPayload = Buffer.from(accountTransactionHandler.serialize(accountTransaction.payload));
} else if (accountTransaction.transactionKind === AccountTransactionType.TransferWithSchedule) {
serializedPayload = serializeSchedule(accountTransaction.payload);
} else if (accountTransaction.transactionKind === AccountTransactionType.TransferWithScheduleAndMemo) {
const scheduleAndMemoResult = serializeScheduleAndMemo(accountTransaction.payload);
serializedPayload = Buffer.concat([scheduleAndMemoResult.addressAndMemo, scheduleAndMemoResult.schedule]);
} else if (accountTransaction.transactionKind === AccountTransactionType.TransferToPublic) {
serializedPayload = serializeTransferToPublic(accountTransaction.payload);
} else if (accountTransaction.transactionKind === AccountTransactionType.TransferWithMemo) {
const transferWithMemoResult = serializeTransferWithMemo(accountTransaction.payload);
serializedPayload = Buffer.concat([transferWithMemoResult.addressAndMemo, transferWithMemoResult.amount]);
} else {
// Fallback for unknown transaction types
throw new Error(`Unsupported transaction type: ${accountTransaction.transactionKind}`);
}
const serializedHeader = serializeAccountTransactionHeader(accountTransaction, serializedPayload.length + 1);
return Buffer.concat([
serializedHeader,
serializedType,
serializedPayload,
]);
}