diff --git a/src/Concordium.ts b/src/Concordium.ts index b9a1e03..904ce56 100644 --- a/src/Concordium.ts +++ b/src/Concordium.ts @@ -6,6 +6,8 @@ import { serializeSimpleTransfer, serializeSimpleTransferWithMemo, serializeTransferWithSchedule, + serializeConfigureBaker, + serializeTransferWithScheduleAndMemo } from "./serialization"; import BigNumber from "bignumber.js"; import { encodeInt32 } from "./utils"; @@ -34,7 +36,12 @@ const INS = { GET_PUBLIC_KEY: 0x01, GET_VERSION: 0x03, GET_APP_NAME: 0x04, - SIGN_TX: 0x06, + SIGN_TRANSFER: 0x06, + SIGN_TRANSFER_MEMO: 0x06, + SIGN_TRANSFER_SCHEDULE: 0x06, + SIGN_TRANSFER_SCHEDULE_AND_MEMO: 0x06, + SIGN_CONFIGURE_DELEGATION: 0x06, + SIGN_CONFIGURE_BAKER: 0x06, }; const concordium_path = "44'/919'/0'/0/0/0"; @@ -150,7 +157,7 @@ export default class Concordium { * @example * concordium.getPublicKey("1105'/0'/0'/0/0/0/0/", true, false) */ - async getPublicKey(path: string, display?: boolean, signedKey?: boolean): Promise<{ publicKey: string }> { + async getPublicKey(path: string, display?: boolean, signedKey?: boolean): Promise<{ publicKey: string, signedPublicKey?: string }> { const pathBuffer = pathToBuffer(path); const publicKeyBuffer = await this.sendToDevice( @@ -162,6 +169,13 @@ export default class Concordium { const publicKeyLength = publicKeyBuffer[0]; + if (signedKey) { + return { + publicKey: publicKeyBuffer.subarray(1, 1 + publicKeyLength).toString("hex"), + signedPublicKey: publicKeyBuffer.subarray(1 + publicKeyLength).toString("hex"), + }; + } + return { publicKey: publicKeyBuffer.subarray(1, 1 + publicKeyLength).toString("hex"), }; @@ -185,7 +199,7 @@ export default class Concordium { for (let i = 0; i < payloads.length; i++) { const lastChunk = i === payloads.length - 1; response = await this.sendToDevice( - INS.SIGN_TX, + INS.SIGN_TRANSFER, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -212,7 +226,7 @@ export default class Concordium { for (let i = 0; i < payloads.length; i++) { const lastChunk = i === payloads.length - 1; response = await this.sendToDevice( - INS.SIGN_TX, + INS.SIGN_TRANSFER_MEMO, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -238,7 +252,34 @@ export default class Concordium { for (let i = 0; i < payloads.length; i++) { const lastChunk = i === payloads.length - 1; response = await this.sendToDevice( - INS.SIGN_TX, + INS.SIGN_TRANSFER_SCHEDULE, + P1_FIRST_CHUNK + i, + lastChunk ? P2_LAST : P2_MORE, + payloads[i] + ); + } + + if (response.length === 1) throw new Error("User has declined."); + + const transaction = payloads.slice(1); + + return { + signature: response.toString("hex"), + transaction: Buffer.concat(transaction).toString("hex"), + }; + } + + async signTransferWithScheduleAndMemo(txn, path: string): Promise<{ signature: string[]; transaction }> { + + + const { payloads } = serializeTransferWithScheduleAndMemo(txn, path); + + let response; + + for (let i = 0; i < payloads.length; i++) { + const lastChunk = i === payloads.length - 1; + response = await this.sendToDevice( + INS.SIGN_TRANSFER_SCHEDULE_AND_MEMO, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -265,7 +306,35 @@ export default class Concordium { for (let i = 0; i < payloads.length; i++) { const lastChunk = i === payloads.length - 1; response = await this.sendToDevice( - INS.SIGN_TX, + INS.SIGN_CONFIGURE_DELEGATION, + P1_FIRST_CHUNK + i, + lastChunk ? P2_LAST : P2_MORE, + payloads[i] + ); + } + + if (response.length === 1) throw new Error("User has declined."); + + const transaction = payloads.slice(1); + + + return { + signature: response.toString("hex"), + transaction: Buffer.concat(transaction).toString("hex"), + }; + } + + async signConfigureBaker(txn, path: string): Promise<{ signature: string[]; transaction }> { + + + const { payloads } = serializeConfigureBaker(txn, path); + + let response; + + for (let i = 0; i < payloads.length; i++) { + const lastChunk = i === payloads.length - 1; + response = await this.sendToDevice( + INS.SIGN_CONFIGURE_BAKER, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] diff --git a/src/serialization.ts b/src/serialization.ts index 432863e..d26b0ca 100644 --- a/src/serialization.ts +++ b/src/serialization.ts @@ -1,8 +1,7 @@ -import BigNumber from "bignumber.js"; import BIPPath from "bip32-path"; import { serializeAccountTransaction } from "./utils"; import { DataBlob } from "@concordium/common-sdk/lib/types/DataBlob"; -import { Buffer as NodeBuffer } from 'buffer/'; +import { Buffer as NodeBuffer } from 'buffer/index'; const MAX_CHUNK_SIZE = 255; const serializePath = (path: number[]): Buffer => { @@ -82,7 +81,7 @@ export const serializeSimpleTransferWithMemo = (txn: any, path: string): { paylo const memo: string = txn.payload.memo; const memoBuffer = NodeBuffer.from(memo, 'utf-8'); // Encode the buffer as a DataBlob - txn.memo = new DataBlob(memoBuffer); + txn.payload.memo = new DataBlob(memoBuffer); return serializeTransaction(txn, path); }; @@ -91,6 +90,19 @@ export const serializeConfigureDelegation = (txn: any, path: string): { payloads return serializeTransaction(txn, path); }; +export const serializeConfigureBaker = (txn: any, path: string): { payloads: Buffer[] } => { + return serializeTransaction(txn, path); +}; + export const serializeTransferWithSchedule = (txn: any, path: string): { payloads: Buffer[] } => { return serializeTransaction(txn, path); }; + +export const serializeTransferWithScheduleAndMemo = (txn: any, path: string): { payloads: Buffer[] } => { + // Convert the string to a buffer + const memo: string = txn.payload.memo; + const memoBuffer = NodeBuffer.from(memo, 'utf-8'); + // Encode the buffer as a DataBlob + txn.payload.memo = new DataBlob(memoBuffer); + return serializeTransaction(txn, path); +}; diff --git a/src/utils.ts b/src/utils.ts index 1b08f9b..35100d9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,18 +25,6 @@ export function isAccountTransactionHandlerExists(transactionKind: AccountTransa } } -/** - * Encodes a 8 bit signed integer to a Buffer using big endian. - * @param value a 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 64 bit unsigned integer to a Buffer using big endian. * @param value a 64 bit integer @@ -60,7 +48,7 @@ export function encodeWord64(value, useLittleEndian = false) { */ 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); + 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); @@ -83,6 +71,39 @@ export function encodeWord32(value, useLittleEndian = false) { 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 a 8 bit signed integer to a Buffer using big endian. + * @param value a 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)); +} + +export function encodeDataBlob(blob) { + const length = encodeWord16(blob.data.length); + return Buffer.concat([length, blob.data]); +} + function serializeSchedule(payload: any) { const toAddressBuffer = AccountAddress.toBuffer(payload.toAddress); const scheduleLength = encodeInt8(payload.schedule.length); @@ -95,6 +116,19 @@ function serializeSchedule(payload: any) { return Buffer.concat([toAddressBuffer, scheduleLength, ...bufferArray]); } +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 Buffer.concat([toAddressBuffer, serializedMemo, scheduleLength, ...bufferArray]); +} + /** * Serialization of an account transaction header. The payload size is a part of the header, * but is factored out of the type as it always has to be derived from the serialized @@ -135,6 +169,8 @@ export const serializeAccountTransaction = (accountTransaction) => { serializedPayload = accountTransactionHandler.serialize(accountTransaction.payload); } else if (accountTransaction.transactionKind === AccountTransactionType.TransferWithSchedule) { serializedPayload = serializeSchedule(accountTransaction.payload); + } else if (accountTransaction.transactionKind === AccountTransactionType.TransferWithScheduleAndMemo) { + serializedPayload = serializeScheduleAndMemo(accountTransaction.payload); } const serializedHeader = serializeAccountTransactionHeader(accountTransaction, serializedPayload.length + 1);