From 7f0614a9ea9333df43d62cc0ec192f7ec9e6e1ae Mon Sep 17 00:00:00 2001 From: GuilaneDen <83951892+GuilaneDen@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:53:50 +0100 Subject: [PATCH 1/9] feat: added signTransferWithScheduleAndMemo, ConfigureBaker --- src/Concordium.ts | 81 ++++++++++++++++++++++++++++++++++++++++---- src/serialization.ts | 18 ++++++++-- src/utils.ts | 62 ++++++++++++++++++++++++++------- 3 files changed, 139 insertions(+), 22 deletions(-) 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); From 54d38d68010b314069486d070daec85dc32fee89 Mon Sep 17 00:00:00 2001 From: GuilaneDen <83951892+GuilaneDen@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:52:23 +0100 Subject: [PATCH 2/9] feat: updated registerData, transferToPublic, signTransferWithMemo, signConfigureDelegation --- src/Concordium.ts | 242 +++++++++++++++++++++++++++++++++---------- src/serialization.ts | 147 +++++++++++++++++++++++--- src/utils.ts | 35 ++++++- 3 files changed, 352 insertions(+), 72 deletions(-) diff --git a/src/Concordium.ts b/src/Concordium.ts index 904ce56..3acdbed 100644 --- a/src/Concordium.ts +++ b/src/Concordium.ts @@ -7,7 +7,11 @@ import { serializeSimpleTransferWithMemo, serializeTransferWithSchedule, serializeConfigureBaker, - serializeTransferWithScheduleAndMemo + serializeTransferWithScheduleAndMemo, + serializeRegisterData, + serializeTransferToPublic, + serializeDeployModule, + serializeInitContract } from "./serialization"; import BigNumber from "bignumber.js"; import { encodeInt32 } from "./utils"; @@ -28,25 +32,34 @@ const P2_SIGNED_KEY = 0x01; // FOR SIGN TRANSACTION const P1_FIRST_CHUNK = 0x00; +const P1_INITIAL_WITH_MEMO = 0x01; +const P1_REMAINING_AMOUNT = 0x01; +const P1_DATA = 0x01; +const P1_PROOF = 0x02; +const P1_MEMO = 0x02; +const P1_AMOUNT = 0x03; const P2_MORE = 0x80; const P2_LAST = 0x00; +const P1_INITIAL_PACKET = 0x00; +const P1_SCHEDULED_TRANSFER_PAIRS = 0x01; const INS = { + // GET_VERSION: 0x03, VERIFY_ADDRESS: 0x00, GET_PUBLIC_KEY: 0x01, - GET_VERSION: 0x03, - GET_APP_NAME: 0x04, - 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, + SIGN_TRANSFER: 0x02, + SIGN_TRANSFER_SCHEDULE: 0x03, + SIGN_TRANSFER_TO_PUBLIC: 0x12, + SIGN_CONFIGURE_DELEGATION: 0x17, + SIGN_CONFIGURE_BAKER: 0x18, + GET_APP_NAME: 0x21, + SIGN_TRANSFER_MEMO: 0x32, + SIGN_TRANSFER_SCHEDULE_AND_MEMO: 0x34, + SIGN_REGISTER_DATA: 0x35, + SIGN_DEPLOY_MODULE: 0x06, + SIGN_INIT_CONTRACT: 0x06, }; -const concordium_path = "44'/919'/0'/0/0/0"; -const concordium_legacy_path = "1105'/0'/0'/0/"; - /** * Concordium API * @@ -77,25 +90,25 @@ export default class Concordium { ); } - /** - * Get application version. - * - * @returns version object - * - * @example - * concordium.getVersion().then(r => r.version) - */ - async getVersion(): Promise<{ version: string }> { - const [major, minor, patch] = await this.sendToDevice( - INS.GET_VERSION, - NONE, - NONE, - Buffer.from([]) - ); - return { - version: `${major}.${minor}.${patch}`, - }; - } + // /** + // * Get application version. + // * + // * @returns version object + // * + // * @example + // * concordium.getVersion().then(r => r.version) + // */ + // async getVersion(): Promise<{ version: string }> { + // const [major, minor, patch] = await this.sendToDevice( + // INS.GET_VERSION, + // NONE, + // NONE, + // Buffer.from([]) + // ); + // return { + // version: `${major}.${minor}.${patch}`, + // }; + // } /** * Legacy Verify address. @@ -162,7 +175,7 @@ export default class Concordium { const publicKeyBuffer = await this.sendToDevice( INS.GET_PUBLIC_KEY, - display ? P1_CONFIRM : P1_NON_CONFIRM, + display ? P1_NON_CONFIRM : P1_CONFIRM, signedKey ? P2_SIGNED_KEY : NONE, pathBuffer ); @@ -216,17 +229,80 @@ export default class Concordium { }; } - async signTransferWithMemo(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signTransferWithMemo(txn, path: string): Promise<{ signature: string[] }> { + + + const { payloadHeaderAddressMemoLength, payloadsMemo, payloadsAmount } = serializeSimpleTransferWithMemo(txn, path); + + let response; + response = await this.sendToDevice( + INS.SIGN_TRANSFER_MEMO, + P1_INITIAL_WITH_MEMO, + NONE, + payloadHeaderAddressMemoLength[0] + ); + response = await this.sendToDevice( + INS.SIGN_TRANSFER_MEMO, + P1_MEMO, + NONE, + payloadsMemo[0] + ); + response = await this.sendToDevice( + INS.SIGN_TRANSFER_MEMO, + P1_AMOUNT, + NONE, + payloadsAmount[0] + ); + + if (response.length === 1) throw new Error("User has declined."); + + return { + signature: response.toString("hex"), + }; + } + + async signTransferWithSchedule(txn, path: string): Promise<{ signature: string[] }> { - const { payloads } = serializeSimpleTransferWithMemo(txn, path); + const { payloadHeaderAddressScheduleLength, payloadsSchedule } = serializeTransferWithSchedule(txn, path); + + let response; + + response = await this.sendToDevice( + INS.SIGN_TRANSFER_SCHEDULE, + P1_INITIAL_PACKET, + NONE, + payloadHeaderAddressScheduleLength[0] + ); + + for (let i = 0; i < payloadsSchedule.length; i++) { + const lastChunk = i === payloadsSchedule.length - 1; + response = await this.sendToDevice( + INS.SIGN_TRANSFER_SCHEDULE, + P1_SCHEDULED_TRANSFER_PAIRS, + NONE, + payloadsSchedule[i] + ); + } + + if (response.length === 1) throw new Error("User has declined."); + + return { + signature: response.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_MEMO, + INS.SIGN_TRANSFER_SCHEDULE_AND_MEMO, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -234,6 +310,7 @@ export default class Concordium { } if (response.length === 1) throw new Error("User has declined."); + const transaction = payloads.slice(1); return { @@ -242,17 +319,17 @@ export default class Concordium { }; } - async signTransferWithSchedule(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signConfigureDelegation(txn, path: string): Promise<{ signature: string[] }> { - const { payloads } = serializeTransferWithSchedule(txn, path); + const { payloads } = serializeConfigureDelegation(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, + INS.SIGN_CONFIGURE_DELEGATION, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -261,25 +338,21 @@ export default class Concordium { 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 }> { - + async signConfigureBaker(txn, path: string): Promise<{ signature: string[]; transaction }> { - const { payloads } = serializeTransferWithScheduleAndMemo(txn, path); + 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_TRANSFER_SCHEDULE_AND_MEMO, + INS.SIGN_CONFIGURE_BAKER, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -290,23 +363,87 @@ export default class Concordium { const transaction = payloads.slice(1); + return { signature: response.toString("hex"), transaction: Buffer.concat(transaction).toString("hex"), }; } - async signConfigureDelegation(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signRegisterData(txn, path: string): Promise<{ signature: string[] }> { + const { payloadHeader, payloadsData } = serializeRegisterData(txn, path); - const { payloads } = serializeConfigureDelegation(txn, path); + let response; + response = await this.sendToDevice( + INS.SIGN_REGISTER_DATA, + P1_INITIAL_PACKET, + NONE, + payloadHeader[0] + ); + + for (let i = 0; i < payloadsData.length; i++) { + response = await this.sendToDevice( + INS.SIGN_REGISTER_DATA, + P1_DATA, + NONE, + payloadsData[i] + ); + } + + if (response.length === 1) throw new Error("User has declined."); + + return { + signature: response.toString("hex"), + }; + } + + async signTransferToPublic(txn, path: string): Promise<{ signature: string[] }> { + + const { payloadHeader, payloadsAmountAndProofsLength, payloadsProofs } = serializeTransferToPublic(txn, path); + + let response; + + response = await this.sendToDevice( + INS.SIGN_TRANSFER_TO_PUBLIC, + P1_INITIAL_PACKET, + NONE, + payloadHeader[0] + ); + + response = await this.sendToDevice( + INS.SIGN_TRANSFER_TO_PUBLIC, + P1_REMAINING_AMOUNT, + NONE, + payloadsAmountAndProofsLength[0] + ); + + for (let i = 0; i < payloadsProofs.length; i++) { + response = await this.sendToDevice( + INS.SIGN_TRANSFER_TO_PUBLIC, + P1_PROOF, + NONE, + payloadsProofs[i] + ); + } + + if (response.length === 1) throw new Error("User has declined."); + + return { + signature: response.toString("hex"), + }; + } + + async signDeployModule(txn, path: string): Promise<{ signature: string[]; transaction }> { + + const { payloads } = serializeDeployModule(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_DELEGATION, + INS.SIGN_DEPLOY_MODULE, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -317,24 +454,22 @@ export default class Concordium { 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 }> { + async signInitContract(txn, path: string): Promise<{ signature: string[]; transaction }> { - - const { payloads } = serializeConfigureBaker(txn, path); + const { payloads } = serializeInitContract(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, + INS.SIGN_INIT_CONTRACT, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -345,7 +480,6 @@ export default class Concordium { const transaction = payloads.slice(1); - return { signature: response.toString("hex"), transaction: Buffer.concat(transaction).toString("hex"), diff --git a/src/serialization.ts b/src/serialization.ts index d26b0ca..cca6bf3 100644 --- a/src/serialization.ts +++ b/src/serialization.ts @@ -1,8 +1,10 @@ import BIPPath from "bip32-path"; -import { serializeAccountTransaction } from "./utils"; +import { encodeDataBlob, encodeInt8, encodeWord16, encodeWord64, serializeAccountTransaction, serializeAccountTransactionHeader } from "./utils"; import { DataBlob } from "@concordium/common-sdk/lib/types/DataBlob"; import { Buffer as NodeBuffer } from 'buffer/index'; +import { AccountAddress } from "@concordium/web-sdk"; const MAX_CHUNK_SIZE = 255; +const MAX_SCHEDULE_CHUNK_SIZE = 15; const serializePath = (path: number[]): Buffer => { const buf = Buffer.alloc(1 + path.length * 4); @@ -35,18 +37,44 @@ export const pathToBuffer = (originalPath: string): Buffer => { return serializePath(pathNums); }; -const serializeTransactionPayloads = ( path: string, rawTx: Buffer): Buffer[] => { +const serializeTransactionPayloadsWithDerivationPath = (path: string, rawTx: Buffer): Buffer[] => { const paths = splitPath(path); let offset = 0; const payloads: Buffer[] = []; - let buffer = Buffer.alloc( - 1 + paths.length * 4 - ); - buffer[0] = paths.length; + let pathBuffer = Buffer.alloc(1 + paths.length * 4); + pathBuffer[0] = paths.length; paths.forEach((element, index) => { - buffer.writeUInt32BE(element, 1 + 4 * index); + pathBuffer.writeUInt32BE(element, 1 + 4 * index); }); - payloads.push(buffer); + + while (offset !== rawTx.length) { + const first = offset === 0; + let chunkSize = + offset + MAX_CHUNK_SIZE > rawTx.length + ? rawTx.length - offset + : MAX_CHUNK_SIZE; + + // Allocate buffer for the first chunk with pathBuffer size + const buffer = Buffer.alloc(first ? pathBuffer.length + chunkSize : chunkSize); + + if (first) { + // Copy pathBuffer to the beginning of the first chunk + pathBuffer.copy(buffer, 0); + rawTx.copy(buffer, pathBuffer.length, offset, offset + chunkSize); + } else { + rawTx.copy(buffer, 0, offset, offset + chunkSize); + } + + payloads.push(buffer); + offset += chunkSize; + } + return payloads; +}; + + +const serializeTransactionPayloads = (rawTx: Buffer): Buffer[] => { + let offset = 0; + const payloads: Buffer[] = []; while (offset !== rawTx.length) { const first = offset === 0; let chunkSize = @@ -66,9 +94,10 @@ const serializeTransactionPayloads = ( path: string, rawTx: Buffer): Buffer[] => return payloads; }; + export const serializeTransaction = (txn: any, path: string): { payloads: Buffer[] } => { const txSerialized = serializeAccountTransaction(txn); - const payloads = serializeTransactionPayloads(path, txSerialized); + const payloads = serializeTransactionPayloadsWithDerivationPath(path, txSerialized); return { payloads }; } @@ -76,14 +105,59 @@ export const serializeSimpleTransfer = (txn: any, path: string): { payloads: Buf return serializeTransaction(txn, path); }; -export const serializeSimpleTransferWithMemo = (txn: any, path: string): { payloads: Buffer[] } => { +export const serializeSimpleTransferWithMemo = (txn: any, path: string): { payloadHeaderAddressMemoLength: Buffer[], payloadsMemo: Buffer[], payloadsAmount: 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); + const serializedType = Buffer.from(Uint8Array.of(txn.transactionKind)); + const serializedToAddress = AccountAddress.toBuffer(txn.payload.toAddress); + const serializedAmount = encodeWord64(txn.payload.amount.microCcdAmount); + const serializedMemo = encodeDataBlob(txn.payload.memo); + const memoLength = serializedMemo.subarray(0, 2); + + const payloadSize = serializedType.length + serializedMemo.length + serializedAmount.length + serializedToAddress.length; + const serializedHeader = serializeAccountTransactionHeader(txn, payloadSize); + const serializedHeaderAddressMemoLength = Buffer.concat([serializedHeader, serializedType, serializedToAddress, memoLength]); + + const payloadHeaderAddressMemoLength = serializeTransactionPayloadsWithDerivationPath(path, serializedHeaderAddressMemoLength); + const payloadsMemo = serializeTransactionPayloads(serializedMemo.subarray(2)); + const payloadsAmount = serializeTransactionPayloads(serializedAmount); + + + + return { payloadHeaderAddressMemoLength, payloadsMemo, payloadsAmount }; +}; + +export const serializeTransferWithSchedule = (txn: any, path: string): { payloadHeaderAddressScheduleLength: Buffer[], payloadsSchedule: Buffer[] } => { + const serializedType = Buffer.from(Uint8Array.of(txn.transactionKind)); + const toAddressBuffer = AccountAddress.toBuffer(txn.payload.toAddress); + const scheduleLength = encodeInt8(txn.payload.schedule.length); + const scheduleBuffer = txn.payload.schedule.map((item: { timestamp: string, amount: string }) => { + const timestampBuffer = encodeWord64(item.timestamp); + const amountBuffer = encodeWord64(item.amount); + return Buffer.concat([timestampBuffer, amountBuffer]); + }); + const serializedSchedule = Buffer.concat([...scheduleBuffer]); + + + const payloadSize = serializedType.length + scheduleLength.length + serializedSchedule.length + toAddressBuffer.length; + const serializedHeader = serializeAccountTransactionHeader(txn, payloadSize); + const serializedHeaderAddressScheduleLength = Buffer.concat([serializedHeader, serializedType, toAddressBuffer, scheduleLength]); + + const payloadHeaderAddressScheduleLength = serializeTransactionPayloadsWithDerivationPath(path, serializedHeaderAddressScheduleLength); + const payloadsSchedule: Buffer[] = []; + + let remainingPairs = txn.payload.schedule.length + for (let i = 0; i < scheduleBuffer.length; i += MAX_SCHEDULE_CHUNK_SIZE) { + const offset = remainingPairs > MAX_SCHEDULE_CHUNK_SIZE ? MAX_SCHEDULE_CHUNK_SIZE : remainingPairs + const scheduleChunk = serializeTransactionPayloads(serializedSchedule.subarray(i * 16, (i + offset) * 16)); + payloadsSchedule.push(...scheduleChunk); + remainingPairs = txn.payload.schedule.length - MAX_SCHEDULE_CHUNK_SIZE + } + return { payloadHeaderAddressScheduleLength, payloadsSchedule }; }; export const serializeConfigureDelegation = (txn: any, path: string): { payloads: Buffer[] } => { @@ -94,9 +168,6 @@ export const serializeConfigureBaker = (txn: any, path: string): { payloads: Buf 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 @@ -106,3 +177,51 @@ export const serializeTransferWithScheduleAndMemo = (txn: any, path: string): { txn.payload.memo = new DataBlob(memoBuffer); return serializeTransaction(txn, path); }; + +export const serializeRegisterData = (txn: any, path: string): { payloadHeader: Buffer[], payloadsData: Buffer[] } => { + // Convert the string to a buffer + const data: string = txn.payload.data; + const dataBuffer = NodeBuffer.from(data, 'utf-8'); + // Encode the buffer as a DataBlob + txn.payload.data = new DataBlob(dataBuffer); + + const serializedData = encodeDataBlob(txn.payload.data); + const serializedType = Buffer.from(Uint8Array.of(txn.transactionKind)); + + const payloadSize = serializedType.length + serializedData.length; + const serializedHeader = serializeAccountTransactionHeader(txn, payloadSize); + const serializedHeaderAndKind = Buffer.concat([serializedHeader, serializedType, serializedData.subarray(0, 2)]); + + const payloadHeader = serializeTransactionPayloadsWithDerivationPath(path, serializedHeaderAndKind); + const payloadsData = serializeTransactionPayloads(serializedData.subarray(2)); + + return { payloadHeader, payloadsData }; +}; + +export const serializeTransferToPublic = (txn: any, path: string): { payloadHeader: Buffer[], payloadsAmountAndProofsLength: Buffer[], payloadsProofs: Buffer[] } => { + const remainingAmount = Buffer.from(txn.payload.remainingAmount, 'hex'); + const transferAmount = encodeWord64(txn.payload.transferAmount.microCcdAmount); + const index = encodeWord64(txn.payload.index); + const proofs = Buffer.from(txn.payload.proofs, 'hex'); + const proofsLength = encodeWord16(proofs.length); + + const serializedType = Buffer.from(Uint8Array.of(txn.transactionKind)); + const payloadSize = remainingAmount.length + transferAmount.length + index.length + proofsLength.length + proofs.length + serializedType.length; + const serializedHeader = serializeAccountTransactionHeader(txn, payloadSize); + const serializedHeaderAndKind = Buffer.concat([serializedHeader, serializedType]); + const serializedAmountAndProofsLength = Buffer.concat([remainingAmount, transferAmount, index, proofsLength]); + + const payloadHeader = serializeTransactionPayloadsWithDerivationPath(path, serializedHeaderAndKind); + const payloadsAmountAndProofsLength = serializeTransactionPayloads(serializedAmountAndProofsLength); + const payloadsProofs = serializeTransactionPayloads(proofs); + + return { payloadHeader, payloadsAmountAndProofsLength, payloadsProofs }; +}; + +export const serializeDeployModule = (txn: any, path: string): { payloads: Buffer[] } => { + return serializeTransaction(txn, path); +}; + +export const serializeInitContract = (txn: any, path: string): { payloads: Buffer[] } => { + return serializeTransaction(txn, path); +}; \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 35100d9..191f534 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -79,7 +79,7 @@ export function encodeWord32(value, useLittleEndian = false) { */ 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); + 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); @@ -126,7 +126,30 @@ function serializeScheduleAndMemo(payload: any) { }); const serializedMemo = encodeDataBlob(payload.memo); - return Buffer.concat([toAddressBuffer, serializedMemo, scheduleLength, ...bufferArray]); + return { + addressAndMemo: Buffer.concat([toAddressBuffer, serializedMemo]), + schedule: Buffer.concat([scheduleLength, ...bufferArray]), + }; +} + +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, + }; +} + +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]); } /** @@ -137,7 +160,7 @@ function serializeScheduleAndMemo(payload: any) { * @param payloadSize the byte size of the serialized payload * @returns the serialized account transaction header */ -const serializeAccountTransactionHeader = (accountTransaction, payloadSize) => { +export const serializeAccountTransactionHeader = (accountTransaction, payloadSize) => { const serializedSender = AccountAddress.toBuffer(accountTransaction.sender); const serializedNonce = encodeWord64(accountTransaction.nonce); const serializedEnergyAmount = encodeWord64(accountTransaction.energyAmount); @@ -164,13 +187,17 @@ export const serializeAccountTransaction = (accountTransaction) => { const serializedType = Buffer.from(Uint8Array.of(accountTransaction.transactionKind)); let serializedPayload; - if (isAccountTransactionHandlerExists(accountTransaction.transactionKind)) { + if (isAccountTransactionHandlerExists(accountTransaction.transactionKind) && accountTransaction.transactionKind !== AccountTransactionType.TransferWithMemo) { const accountTransactionHandler = getAccountTransactionHandler(accountTransaction.transactionKind); 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); + } else if (accountTransaction.transactionKind === AccountTransactionType.TransferToPublic) { + serializedPayload = serializeTransferToPublic(accountTransaction.payload); + } else if (accountTransaction.transactionKind === AccountTransactionType.TransferWithMemo) { + serializedPayload = serializeTransferWithMemo(accountTransaction.payload); } const serializedHeader = serializeAccountTransactionHeader(accountTransaction, serializedPayload.length + 1); From 4e60467724c591d9557995c860f573b8f8fdedd8 Mon Sep 17 00:00:00 2001 From: GuilaneDen <83951892+GuilaneDen@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:34:18 +0100 Subject: [PATCH 3/9] feat: updated signTransferWithScheduleAndMemo --- src/Concordium.ts | 30 ++++++++++++++++++++---------- src/serialization.ts | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/Concordium.ts b/src/Concordium.ts index 3acdbed..8959e83 100644 --- a/src/Concordium.ts +++ b/src/Concordium.ts @@ -33,6 +33,8 @@ const P2_SIGNED_KEY = 0x01; // FOR SIGN TRANSACTION const P1_FIRST_CHUNK = 0x00; const P1_INITIAL_WITH_MEMO = 0x01; +const P1_INITIAL_WITH_MEMO_SCHEDULE = 0x02; +const P1_MEMO_SCHEDULE = 0x03; const P1_REMAINING_AMOUNT = 0x01; const P1_DATA = 0x01; const P1_PROOF = 0x02; @@ -292,30 +294,38 @@ export default class Concordium { }; } - async signTransferWithScheduleAndMemo(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signTransferWithScheduleAndMemo(txn, path: string): Promise<{ signature: string[] }> { - const { payloads } = serializeTransferWithScheduleAndMemo(txn, path); + const { payloadHeaderAddressScheduleLengthAndMemoLength, payloadMemo, payloadsSchedule } = serializeTransferWithScheduleAndMemo(txn, path); let response; + response = await this.sendToDevice( + INS.SIGN_TRANSFER_SCHEDULE_AND_MEMO, + P1_INITIAL_WITH_MEMO_SCHEDULE, + NONE, + payloadHeaderAddressScheduleLengthAndMemoLength[0] + ); + response = await this.sendToDevice( + INS.SIGN_TRANSFER_SCHEDULE_AND_MEMO, + P1_MEMO_SCHEDULE, + NONE, + payloadMemo[0] + ); - for (let i = 0; i < payloads.length; i++) { - const lastChunk = i === payloads.length - 1; + for (let i = 0; i < payloadsSchedule.length; i++) { response = await this.sendToDevice( INS.SIGN_TRANSFER_SCHEDULE_AND_MEMO, - P1_FIRST_CHUNK + i, - lastChunk ? P2_LAST : P2_MORE, - payloads[i] + P1_SCHEDULED_TRANSFER_PAIRS, + NONE, + payloadsSchedule[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"), }; } diff --git a/src/serialization.ts b/src/serialization.ts index cca6bf3..e0e8c41 100644 --- a/src/serialization.ts +++ b/src/serialization.ts @@ -169,13 +169,42 @@ export const serializeConfigureBaker = (txn: any, path: string): { payloads: Buf }; -export const serializeTransferWithScheduleAndMemo = (txn: any, path: string): { payloads: Buffer[] } => { +export const serializeTransferWithScheduleAndMemo = (txn: any, path: string): { payloadHeaderAddressScheduleLengthAndMemoLength: Buffer[], payloadMemo: Buffer[], payloadsSchedule: 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); + + const toAddressBuffer = AccountAddress.toBuffer(txn.payload.toAddress); + const scheduleLength = encodeInt8(txn.payload.schedule.length); + const scheduleBufferArray = txn.payload.schedule.map((item: { timestamp: string, amount: string }) => { + const timestampBuffer = encodeWord64(item.timestamp); + const amountBuffer = encodeWord64(item.amount); + return Buffer.concat([timestampBuffer, amountBuffer]); + }); + + const serializedSchedule = Buffer.concat([...scheduleBufferArray]); + const serializedMemo = encodeDataBlob(txn.payload.memo); + const serializedType = Buffer.from(Uint8Array.of(txn.transactionKind)); + + const payloadSize = serializedType.length + scheduleLength.length + serializedSchedule.length + toAddressBuffer.length + serializedMemo.length; + const serializedHeader = serializeAccountTransactionHeader(txn, payloadSize); + const serializedHeaderAddressScheduleLengthAndMemoLength = Buffer.concat([serializedHeader, serializedType, toAddressBuffer, scheduleLength, serializedMemo.subarray(0, 2)]); + + const payloadHeaderAddressScheduleLengthAndMemoLength = serializeTransactionPayloadsWithDerivationPath(path, serializedHeaderAddressScheduleLengthAndMemoLength); + const payloadMemo = serializeTransactionPayloads(serializedMemo.subarray(2)); + const payloadsSchedule: Buffer[] = []; + + let remainingPairs = txn.payload.schedule.length + for (let i = 0; i < scheduleBufferArray.length; i += MAX_SCHEDULE_CHUNK_SIZE) { + const offset = remainingPairs > MAX_SCHEDULE_CHUNK_SIZE ? MAX_SCHEDULE_CHUNK_SIZE : remainingPairs + const scheduleChunk = serializeTransactionPayloads(serializedSchedule.subarray(i * 16, (i + offset) * 16)); + payloadsSchedule.push(...scheduleChunk); + remainingPairs = txn.payload.schedule.length - MAX_SCHEDULE_CHUNK_SIZE + } + + return { payloadHeaderAddressScheduleLengthAndMemoLength, payloadMemo, payloadsSchedule }; }; export const serializeRegisterData = (txn: any, path: string): { payloadHeader: Buffer[], payloadsData: Buffer[] } => { From 17dac31da3915a854e5d45c1e5f9afb07cc6b056 Mon Sep 17 00:00:00 2001 From: GuilaneDen <83951892+GuilaneDen@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:25:36 +0100 Subject: [PATCH 4/9] feat: updated configureBaker --- src/Concordium.ts | 60 ++++++++++++++++++++++++++--------- src/serialization.ts | 74 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 117 insertions(+), 17 deletions(-) diff --git a/src/Concordium.ts b/src/Concordium.ts index 8959e83..e5689ba 100644 --- a/src/Concordium.ts +++ b/src/Concordium.ts @@ -31,6 +31,12 @@ const P1_CONFIRM = 0x01; const P2_SIGNED_KEY = 0x01; // FOR SIGN TRANSACTION +const P1_FIRST_BATCH = 0x01; +const P1_AGGREGATION_KEY = 0x02; +const P1_URL_LENGTH = 0x03; +const P1_URL = 0x04; +const P1_COMMISSION_FEE = 0x05; + const P1_FIRST_CHUNK = 0x00; const P1_INITIAL_WITH_MEMO = 0x01; const P1_INITIAL_WITH_MEMO_SCHEDULE = 0x02; @@ -45,6 +51,7 @@ const P2_LAST = 0x00; const P1_INITIAL_PACKET = 0x00; const P1_SCHEDULED_TRANSFER_PAIRS = 0x01; + const INS = { // GET_VERSION: 0x03, VERIFY_ADDRESS: 0x00, @@ -353,30 +360,53 @@ export default class Concordium { }; } - async signConfigureBaker(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signConfigureBaker(txn, path: string): Promise<{ signature: string[] }> { - const { payloads } = serializeConfigureBaker(txn, path); + const { payloadHeaderKindAndBitmap, payloadFirstBatch, payloadAggregationKeys, payloadUrlLength, payloadURL, payloadCommissionFee } = 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] - ); - } + response = await this.sendToDevice( + INS.SIGN_CONFIGURE_BAKER, + P1_INITIAL_PACKET, + NONE, + payloadHeaderKindAndBitmap + ); + response = await this.sendToDevice( + INS.SIGN_CONFIGURE_BAKER, + P1_FIRST_BATCH, + NONE, + payloadFirstBatch + ); + response = await this.sendToDevice( + INS.SIGN_CONFIGURE_BAKER, + P1_AGGREGATION_KEY, + NONE, + payloadAggregationKeys + ); + response = await this.sendToDevice( + INS.SIGN_CONFIGURE_BAKER, + P1_URL_LENGTH, + NONE, + payloadUrlLength + ); + response = await this.sendToDevice( + INS.SIGN_CONFIGURE_BAKER, + P1_URL, + NONE, + payloadURL + ); + response = await this.sendToDevice( + INS.SIGN_CONFIGURE_BAKER, + P1_COMMISSION_FEE, + NONE, + payloadCommissionFee + ); 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"), }; } diff --git a/src/serialization.ts b/src/serialization.ts index e0e8c41..237e3ae 100644 --- a/src/serialization.ts +++ b/src/serialization.ts @@ -3,8 +3,22 @@ import { encodeDataBlob, encodeInt8, encodeWord16, encodeWord64, serializeAccoun import { DataBlob } from "@concordium/common-sdk/lib/types/DataBlob"; import { Buffer as NodeBuffer } from 'buffer/index'; import { AccountAddress } from "@concordium/web-sdk"; + const MAX_CHUNK_SIZE = 255; const MAX_SCHEDULE_CHUNK_SIZE = 15; +const HEADER_LENGTH = 60; +const TRANSACTION_KIND_LENGTH = 1; +const BITMAP_LENGTH = 2; +const STAKING_PAYLOAD_LENGTH = 8; +const RESTAKE_EARNINGS_PAYLOAD_LENGTH = 1; +const OPEN_FOR_DELEGATION_PAYLOAD_LENGTH = 1; +const KEYS_AGGREGATION_LENGTH = 160; +const KEYS_ELECTION_AND_SIGNATURE_LENGTH = 192; +const KEYS_PAYLOAD_LENGTH = KEYS_ELECTION_AND_SIGNATURE_LENGTH + KEYS_AGGREGATION_LENGTH; +const METADATA_URL_LENGTH = 2; +const TRANSACTION_FEE_COMMISSION_LENGTH = 4; +const BAKING_REWARD_COMMISSION_LENGTH = 4; +const FINALIZATION_REWARD_COMMISSION_LENGTH = 4; const serializePath = (path: number[]): Buffer => { const buf = Buffer.alloc(1 + path.length * 4); @@ -164,8 +178,64 @@ 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 serializeConfigureBaker = (txn: any, path: string): { payloadHeaderKindAndBitmap: Buffer, payloadFirstBatch: Buffer, payloadAggregationKeys: Buffer, payloadUrlLength: Buffer, payloadURL: Buffer, payloadCommissionFee: Buffer } => { + let stake: Buffer = Buffer.alloc(0); + let restakeEarnings: Buffer = Buffer.alloc(0); + let openForDelegation: Buffer = Buffer.alloc(0); + let keys: Buffer = Buffer.alloc(0); + let metadataUrl: Buffer = Buffer.alloc(0); + let url: Buffer = Buffer.alloc(0); + let transactionFeeCommission: Buffer = Buffer.alloc(0); + let bakingRewardCommission: Buffer = Buffer.alloc(0); + let finalizationRewardCommission: Buffer = Buffer.alloc(0); + let offset: number = 0; + + const txSerialized = serializeAccountTransaction(txn); + const headerKindAndBitmap = txSerialized.subarray(0, HEADER_LENGTH + TRANSACTION_KIND_LENGTH + BITMAP_LENGTH); + offset += HEADER_LENGTH + TRANSACTION_KIND_LENGTH + BITMAP_LENGTH; + if (txn.payload.hasOwnProperty('stake')) { + stake = txSerialized.subarray(offset, offset + STAKING_PAYLOAD_LENGTH); + offset += STAKING_PAYLOAD_LENGTH; + } + if (txn.payload.hasOwnProperty('restakeEarnings')) { + restakeEarnings = txSerialized.subarray(offset, offset + RESTAKE_EARNINGS_PAYLOAD_LENGTH); + offset += RESTAKE_EARNINGS_PAYLOAD_LENGTH; + } + if (txn.payload.hasOwnProperty('openForDelegation')) { + openForDelegation = txSerialized.subarray(offset, offset + OPEN_FOR_DELEGATION_PAYLOAD_LENGTH); + offset += OPEN_FOR_DELEGATION_PAYLOAD_LENGTH; + } + if (txn.payload.hasOwnProperty('keys')) { + keys = txSerialized.subarray(offset, offset + KEYS_PAYLOAD_LENGTH); + offset += KEYS_PAYLOAD_LENGTH; + } + if (txn.payload.hasOwnProperty('metadataUrl')) { + metadataUrl = txSerialized.subarray(offset, offset + METADATA_URL_LENGTH); + offset += METADATA_URL_LENGTH; + url = txSerialized.subarray(offset, offset + metadataUrl.readUInt16BE(0)); + offset += metadataUrl.readUInt16BE(0); + } + if (txn.payload.hasOwnProperty('transactionFeeCommission')) { + transactionFeeCommission = txSerialized.subarray(offset, offset + TRANSACTION_FEE_COMMISSION_LENGTH); + offset += TRANSACTION_FEE_COMMISSION_LENGTH; + } + if (txn.payload.hasOwnProperty('bakingRewardCommission')) { + bakingRewardCommission = txSerialized.subarray(offset, offset + BAKING_REWARD_COMMISSION_LENGTH); + offset += BAKING_REWARD_COMMISSION_LENGTH; + } + if (txn.payload.hasOwnProperty('finalizationRewardCommission')) { + finalizationRewardCommission = txSerialized.subarray(offset, offset + FINALIZATION_REWARD_COMMISSION_LENGTH); + offset += FINALIZATION_REWARD_COMMISSION_LENGTH; + } + + const payloadHeaderKindAndBitmap = serializeTransactionPayloadsWithDerivationPath(path, headerKindAndBitmap); + const payloadFirstBatch = Buffer.concat([stake, restakeEarnings, openForDelegation, keys.subarray(0, KEYS_ELECTION_AND_SIGNATURE_LENGTH)]); + const payloadAggregationKeys = keys.subarray(KEYS_ELECTION_AND_SIGNATURE_LENGTH); + const payloadUrlLength = metadataUrl; + const payloadURL = url; + const payloadCommissionFee = Buffer.concat([transactionFeeCommission, bakingRewardCommission, finalizationRewardCommission]); + + return { payloadHeaderKindAndBitmap: payloadHeaderKindAndBitmap[0], payloadFirstBatch, payloadAggregationKeys, payloadUrlLength, payloadURL, payloadCommissionFee }; }; From de39666482dc23228c4a34f60da9ab242603e886 Mon Sep 17 00:00:00 2001 From: GuilaneDen <83951892+GuilaneDen@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:14:28 +0100 Subject: [PATCH 5/9] feat: implemented updateCredential --- .github/workflows/main.yaml | 6 +- src/Concordium.ts | 167 ++++++++++++++++++++++++++++++++++-- src/serialization.ts | 87 ++++++++++++++++++- 3 files changed, 247 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 46f5ab5..50121d4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -21,13 +21,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: true - name: Install node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: - node-version: "^16.10.0" + node-version: "22.x" - name: Install dependencies run: yarn install - name: Run test diff --git a/src/Concordium.ts b/src/Concordium.ts index e5689ba..e1401cb 100644 --- a/src/Concordium.ts +++ b/src/Concordium.ts @@ -11,7 +11,10 @@ import { serializeRegisterData, serializeTransferToPublic, serializeDeployModule, - serializeInitContract + serializeInitContract, + serializeUpdateContract, + serializeTransactionPayloads, + serializeUpdateCredentials } from "./serialization"; import BigNumber from "bignumber.js"; import { encodeInt32 } from "./utils"; @@ -51,6 +54,25 @@ const P2_LAST = 0x00; const P1_INITIAL_PACKET = 0x00; const P1_SCHEDULED_TRANSFER_PAIRS = 0x01; +// Update Credentials +const P2_CREDENTIAL_INITIAL = 0x00; +const P2_CREDENTIAL_CREDENTIAL_INDEX = 0x01; +const P2_CREDENTIAL_CREDENTIAL = 0x02; +const P2_CREDENTIAL_ID_COUNT = 0x03; +const P2_CREDENTIAL_ID = 0x04; +const P2_THRESHOLD = 0x05; + +//Deploy Credential +const P1_VERIFICATION_KEY_LENGTH = 0x0A; +const P1_VERIFICATION_KEY = 0x01; +const P1_SIGNATURE_THRESHOLD = 0x02; +const P1_AR_IDENTITY = 0x03; +const P1_CREDENTIAL_DATES = 0x04; +const P1_ATTRIBUTE_TAG = 0x05; +const P1_ATTRIBUTE_VALUE = 0x06; +const P1_LENGTH_OF_PROOFS = 0x07; +const P1_PROOFS = 0x08; +const P1_NEW_OR_EXISTING = 0x09 const INS = { // GET_VERSION: 0x03, @@ -62,11 +84,13 @@ const INS = { SIGN_CONFIGURE_DELEGATION: 0x17, SIGN_CONFIGURE_BAKER: 0x18, GET_APP_NAME: 0x21, + SIGN_UPDATE_CREDENTIALS: 0x31, SIGN_TRANSFER_MEMO: 0x32, SIGN_TRANSFER_SCHEDULE_AND_MEMO: 0x34, SIGN_REGISTER_DATA: 0x35, SIGN_DEPLOY_MODULE: 0x06, SIGN_INIT_CONTRACT: 0x06, + SIGN_UPDATE_CONTRACT: 0x06, }; /** @@ -474,7 +498,7 @@ export default class Concordium { }; } - async signDeployModule(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signDeployModule(txn, path: string): Promise<{ signature: string[] }> { const { payloads } = serializeDeployModule(txn, path); @@ -492,15 +516,12 @@ export default class Concordium { 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 signInitContract(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signInitContract(txn, path: string): Promise<{ signature: string[] }> { const { payloads } = serializeInitContract(txn, path); @@ -518,11 +539,141 @@ export default class Concordium { if (response.length === 1) throw new Error("User has declined."); - const transaction = payloads.slice(1); + return { + signature: response.toString("hex"), + }; + } + + async signUpdateContract(txn, path: string): Promise<{ signature: string[] }> { + + const { payloads } = serializeUpdateContract(txn, path); + + let response; + + for (let i = 0; i < payloads.length; i++) { + const lastChunk = i === payloads.length - 1; + response = await this.sendToDevice( + INS.SIGN_UPDATE_CONTRACT, + P1_FIRST_CHUNK + i, + lastChunk ? P2_LAST : P2_MORE, + payloads[i] + ); + } + + if (response.length === 1) throw new Error("User has declined."); + + return { + signature: response.toString("hex"), + }; + } + + async signUpdateCredentials(txn, path: string): Promise<{ signature: string[] }> { + + const { payloadHeaderKindAndIndexLength, credentialIndex, numberOfVerificationKeys, keyIndexAndSchemeAndVerificationKey, thresholdAndRegIdAndIPIdentity, encIdCredPubShareAndKey, validToAndCreatedAtAndAttributesLength, tag, valueLength, value, proofLength, proofs, credentialIdCount, credentialIds, threshold } = serializeUpdateCredentials(txn, path); + + let response; + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + NONE, + P2_CREDENTIAL_INITIAL, + payloadHeaderKindAndIndexLength[0] + ); + + for (let i = 0; i < txn.payload.newCredentials.length; i++) { + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + NONE, + P2_CREDENTIAL_CREDENTIAL_INDEX, + credentialIndex[i] + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_VERIFICATION_KEY_LENGTH, + P2_CREDENTIAL_CREDENTIAL, + numberOfVerificationKeys[i] + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_VERIFICATION_KEY, + P2_CREDENTIAL_CREDENTIAL, + keyIndexAndSchemeAndVerificationKey[i] + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_SIGNATURE_THRESHOLD, + P2_CREDENTIAL_CREDENTIAL, + thresholdAndRegIdAndIPIdentity[i] + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_AR_IDENTITY, + P2_CREDENTIAL_CREDENTIAL, + encIdCredPubShareAndKey[i] + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_CREDENTIAL_DATES, + P2_CREDENTIAL_CREDENTIAL, + validToAndCreatedAtAndAttributesLength[i] + ); + for (let j = 0; j < Object.keys(txn.payload.newCredentials[i].cdi.policy.revealedAttributes).length; j++) { + const tagAndValueLength = Buffer.concat([tag[i][j], valueLength[i][j]]) + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_ATTRIBUTE_TAG, + P2_CREDENTIAL_CREDENTIAL, + tagAndValueLength + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_ATTRIBUTE_VALUE, + P2_CREDENTIAL_CREDENTIAL, + value[i][j] + ); + } + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_LENGTH_OF_PROOFS, + P2_CREDENTIAL_CREDENTIAL, + proofLength[i] + ); + + const proofPayload = serializeTransactionPayloads(proofs[i]); + for (let j = 0; j < proofPayload.length; j++) { + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_PROOFS, + P2_CREDENTIAL_CREDENTIAL, + proofPayload[j] + ); + } + } + + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + NONE, + P2_CREDENTIAL_ID_COUNT, + credentialIdCount + ); + for (let i = 0; i < txn.payload.removeCredentialIds.length; i++) { + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + NONE, + P2_CREDENTIAL_ID, + credentialIds[i] + ); + } + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + NONE, + P2_THRESHOLD, + threshold + ); + + if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), - transaction: Buffer.concat(transaction).toString("hex"), }; } diff --git a/src/serialization.ts b/src/serialization.ts index 237e3ae..29386f6 100644 --- a/src/serialization.ts +++ b/src/serialization.ts @@ -8,6 +8,8 @@ const MAX_CHUNK_SIZE = 255; const MAX_SCHEDULE_CHUNK_SIZE = 15; const HEADER_LENGTH = 60; const TRANSACTION_KIND_LENGTH = 1; +const INDEX_LENGTH = 1; +const ONE_OCTET_LENGTH = 1; const BITMAP_LENGTH = 2; const STAKING_PAYLOAD_LENGTH = 8; const RESTAKE_EARNINGS_PAYLOAD_LENGTH = 1; @@ -18,7 +20,20 @@ const KEYS_PAYLOAD_LENGTH = KEYS_ELECTION_AND_SIGNATURE_LENGTH + KEYS_AGGREGATIO const METADATA_URL_LENGTH = 2; const TRANSACTION_FEE_COMMISSION_LENGTH = 4; const BAKING_REWARD_COMMISSION_LENGTH = 4; +const REVOCATION_THRESHOLD_LENGTH = 4; const FINALIZATION_REWARD_COMMISSION_LENGTH = 4; +const KEY_LENGTH = 32; +const REG_ID_LENGTH = 48; +const IP_IDENTITY_LENGTH = 4; +const AR_DATA_LENGTH = 2; +const ID_CRED_PUB_SHARE_LENGTH = 96; +const VALID_TO_LENGTH = 3; +const CREATED_AT_LENGTH = 3; +const ATTRIBUTES_LENGTH = 2; +const TAG_LENGTH = 1; +const VALUE_LENGTH = 1; +const PROOF_LENGTH_LENGTH = 4; +const CREDENTIAL_ID_LENGTH = 48; const serializePath = (path: number[]): Buffer => { const buf = Buffer.alloc(1 + path.length * 4); @@ -86,7 +101,7 @@ const serializeTransactionPayloadsWithDerivationPath = (path: string, rawTx: Buf }; -const serializeTransactionPayloads = (rawTx: Buffer): Buffer[] => { +export const serializeTransactionPayloads = (rawTx: Buffer): Buffer[] => { let offset = 0; const payloads: Buffer[] = []; while (offset !== rawTx.length) { @@ -323,4 +338,72 @@ export const serializeDeployModule = (txn: any, path: string): { payloads: Buffe export const serializeInitContract = (txn: any, path: string): { payloads: Buffer[] } => { return serializeTransaction(txn, path); -}; \ No newline at end of file +}; + +export const serializeUpdateContract = (txn: any, path: string): { payloads: Buffer[] } => { + return serializeTransaction(txn, path); +}; + +export const serializeUpdateCredentials = (txn: any, path: string): { payloadHeaderKindAndIndexLength: Buffer[], credentialIndex: Buffer[], numberOfVerificationKeys: Buffer[], keyIndexAndSchemeAndVerificationKey: Buffer[], thresholdAndRegIdAndIPIdentity: Buffer[], encIdCredPubShareAndKey: Buffer[], validToAndCreatedAtAndAttributesLength: Buffer[], attributesLength: Buffer[], tag: Buffer[][], valueLength: Buffer[][], value: Buffer[][], proofLength: Buffer[], proofs: Buffer[], credentialIdCount: Buffer, credentialIds: Buffer[], threshold: Buffer } => { + let offset = 0; + const txSerialized = serializeAccountTransaction(txn); + const headerKindAndIndexLength = txSerialized.subarray(offset, offset + HEADER_LENGTH + TRANSACTION_KIND_LENGTH + INDEX_LENGTH); + const payloadHeaderKindAndIndexLength = serializeTransactionPayloadsWithDerivationPath(path, headerKindAndIndexLength); + offset += HEADER_LENGTH + TRANSACTION_KIND_LENGTH + INDEX_LENGTH; + + let credentialIndex: Buffer[] = []; + let numberOfVerificationKeys: Buffer[] = []; + let keyIndexAndSchemeAndVerificationKey: Buffer[] = []; + let thresholdAndRegIdAndIPIdentity: Buffer[] = []; + let encIdCredPubShareAndKey: Buffer[] = []; + let validToAndCreatedAtAndAttributesLength: Buffer[] = []; + let attributesLength: Buffer[] = []; + let tag: Buffer[][] = [[]]; + let valueLength: Buffer[][] = [[]]; + let value: Buffer[][] = [[]]; + let proofLength: Buffer[] = []; + let proofs: Buffer[] = []; + + for (let i = 0; i < txn.payload.newCredentials.length; i++) { + credentialIndex[i] = txSerialized.subarray(offset, offset + INDEX_LENGTH); + offset += INDEX_LENGTH; + numberOfVerificationKeys[i] = txSerialized.subarray(offset, offset + INDEX_LENGTH); + offset += INDEX_LENGTH; + keyIndexAndSchemeAndVerificationKey[i] = txSerialized.subarray(offset, offset + 2 * ONE_OCTET_LENGTH + KEY_LENGTH); + offset += 2 * ONE_OCTET_LENGTH + KEY_LENGTH; + thresholdAndRegIdAndIPIdentity[i] = txSerialized.subarray(offset, offset + 2 * ONE_OCTET_LENGTH + REG_ID_LENGTH + IP_IDENTITY_LENGTH + AR_DATA_LENGTH); + offset += 2 * ONE_OCTET_LENGTH + REG_ID_LENGTH + IP_IDENTITY_LENGTH + AR_DATA_LENGTH; + encIdCredPubShareAndKey[i] = txSerialized.subarray(offset, offset + 4 * ONE_OCTET_LENGTH + ID_CRED_PUB_SHARE_LENGTH); + offset += 4 * ONE_OCTET_LENGTH + ID_CRED_PUB_SHARE_LENGTH; + validToAndCreatedAtAndAttributesLength[i] = txSerialized.subarray(offset, offset + ATTRIBUTES_LENGTH + VALID_TO_LENGTH + CREATED_AT_LENGTH); + offset += ATTRIBUTES_LENGTH + VALID_TO_LENGTH + CREATED_AT_LENGTH; + attributesLength[i] = validToAndCreatedAtAndAttributesLength[i].subarray(-ATTRIBUTES_LENGTH); + tag[i] = []; + valueLength[i] = []; + value[i] = []; + for (let j = 0; j < attributesLength[i].readUInt16BE(0); j++) { + tag[i].push(txSerialized.subarray(offset, offset + TAG_LENGTH)); + offset += TAG_LENGTH; + valueLength[i].push(txSerialized.subarray(offset, offset + VALUE_LENGTH)); + offset += VALUE_LENGTH; + value[i].push(txSerialized.subarray(offset, offset + valueLength[i][j].readUInt8(0))); + offset += valueLength[i][j].readUInt8(0); + } + + proofLength[i] = txSerialized.subarray(offset, offset + PROOF_LENGTH_LENGTH); + offset += PROOF_LENGTH_LENGTH; + proofs[i] = txSerialized.subarray(offset, offset + proofLength[i].readUInt32BE(0)); + offset += proofLength[i].readUInt32BE(0); + } + const credentialIdCount = txSerialized.subarray(offset, offset + ONE_OCTET_LENGTH); + offset += ONE_OCTET_LENGTH; + + const credentialIds: Buffer[] = []; + for (let i = 0; i < credentialIdCount.readUInt8(0); i++) { + credentialIds.push(txSerialized.subarray(offset, offset + CREDENTIAL_ID_LENGTH)); + offset += CREDENTIAL_ID_LENGTH; + } + const threshold = txSerialized.subarray(offset, offset + ONE_OCTET_LENGTH); + offset += ONE_OCTET_LENGTH; + return { payloadHeaderKindAndIndexLength, credentialIndex, numberOfVerificationKeys, keyIndexAndSchemeAndVerificationKey, thresholdAndRegIdAndIPIdentity, encIdCredPubShareAndKey, validToAndCreatedAtAndAttributesLength, attributesLength, tag, valueLength, value, proofLength, proofs, credentialIdCount, credentialIds, threshold }; +}; From 3499e0e505b6f41eafa50ed8647d7653bfd8ecd5 Mon Sep 17 00:00:00 2001 From: GuilaneDen <83951892+GuilaneDen@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:32:55 +0100 Subject: [PATCH 6/9] feat: implemented credentialDeployment --- .github/workflows/publish.yaml | 4 +- package-lock.json | 4 +- package.json | 25 ++++---- src/Concordium.ts | 110 ++++++++++++++++++++++++++++++++- src/serialization.ts | 50 ++++++++++++++- 5 files changed, 174 insertions(+), 19 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 6d3d316..c20508a 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -20,11 +20,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: true - name: Install node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: '16' registry-url: "https://registry.npmjs.org/" diff --git a/package-lock.json b/package-lock.json index a2755c7..7caa0c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@ledgerhq/hw-app-algorand", + "name": "@ledgerhq/hw-app-concordium", "version": "6.29.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@ledgerhq/hw-app-algorand", + "name": "@ledgerhq/hw-app-concordium", "version": "6.29.4", "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index dc74751..b01dace 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,34 @@ { - "name": "@ledgerhq/hw-app-algorand", + "name": "@ledgerhq/hw-app-concordium", "type": "module", "version": "6.29.4", - "description": "Ledger Hardware Wallet Algorand Application API", + "description": "Ledger Hardware Wallet Concordium Application API", "keywords": [ "Ledger", "LedgerWallet", - "algo", - "Algorand", - "NanoS", + "CCD", + "Concordium", + "NanoS+", + "NanoX", + "Flex", + "Stax", "Blue", "Hardware Wallet" ], "repository": { "type": "git", - "url": "https://github.com/LedgerHQ/ledger-live.git" + "url": "https://github.com/blooo-io/hw-app-concordium.git" }, "bugs": { - "url": "https://github.com/LedgerHQ/ledger-live/issues" + "url": "https://github.com/blooo-io/hw-app-concordium/issues" }, - "homepage": "https://github.com/LedgerHQ/ledger-live/tree/develop/libs/ledgerjs/packages/hw-app-algorand", + "homepage": "https://github.com/blooo-io/hw-app-concordium", "publishConfig": { "access": "public" }, - "main": "lib/Algorand.js", - "module": "lib-es/Algorand.js", - "types": "lib/Algorand.d.ts", + "main": "lib/Concordium.js", + "module": "lib-es/Concordium.js", + "types": "lib/Concordium.d.ts", "license": "Apache-2.0", "dependencies": { "@concordium/common-sdk": "^9.5.3", diff --git a/src/Concordium.ts b/src/Concordium.ts index e1401cb..7b1d1f7 100644 --- a/src/Concordium.ts +++ b/src/Concordium.ts @@ -14,10 +14,10 @@ import { serializeInitContract, serializeUpdateContract, serializeTransactionPayloads, - serializeUpdateCredentials + serializeUpdateCredentials, + serializeCredentialDeployment } from "./serialization"; -import BigNumber from "bignumber.js"; -import { encodeInt32 } from "./utils"; +import { encodeInt32, encodeInt8, encodeWord64 } from "./utils"; const LEDGER_CLA = 0xe0; @@ -80,6 +80,7 @@ const INS = { GET_PUBLIC_KEY: 0x01, SIGN_TRANSFER: 0x02, SIGN_TRANSFER_SCHEDULE: 0x03, + SIGN_CREDENTIAL_DEPLOYMENT: 0x04, SIGN_TRANSFER_TO_PUBLIC: 0x12, SIGN_CONFIGURE_DELEGATION: 0x17, SIGN_CONFIGURE_BAKER: 0x18, @@ -567,6 +568,109 @@ export default class Concordium { }; } + async signCredentialDeployment(txn, isNew: boolean, addressOrExpiry: string | BigInt, path: string): Promise<{ signature: string[] }> { + + const { payloadDerivationPath, numberOfVerificationKeys, keyIndexAndSchemeAndVerificationKey, thresholdAndRegIdAndIPIdentity, encIdCredPubShareAndKey, validToAndCreatedAtAndAttributesLength, tag, valueLength, value, proofLength, proofs } = serializeCredentialDeployment(txn, path); + + let response; + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_INITIAL_PACKET, + NONE, + payloadDerivationPath + ); + + + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_VERIFICATION_KEY_LENGTH, + NONE, + numberOfVerificationKeys + ); + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_VERIFICATION_KEY, + NONE, + keyIndexAndSchemeAndVerificationKey + ); + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_SIGNATURE_THRESHOLD, + NONE, + thresholdAndRegIdAndIPIdentity + ); + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_AR_IDENTITY, + NONE, + encIdCredPubShareAndKey + ); + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_CREDENTIAL_DATES, + NONE, + validToAndCreatedAtAndAttributesLength + ); + for (let i = 0; i < Object.keys(txn.policy.revealedAttributes).length; i++) { + const tagAndValueLength = Buffer.concat([tag[i], valueLength[i]]) + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_ATTRIBUTE_TAG, + NONE, + tagAndValueLength + ); + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_ATTRIBUTE_VALUE, + NONE, + value[i] + ); + } + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_LENGTH_OF_PROOFS, + NONE, + proofLength + ); + + const proofPayload = serializeTransactionPayloads(proofs); + for (let i = 0; i < proofPayload.length; i++) { + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_PROOFS, + NONE, + proofPayload[i] + ); + } + + if (isNew) { + const isNew = encodeInt8(0); + const serializeExpiry = encodeWord64(addressOrExpiry as BigInt); + const expiry = Buffer.concat([isNew, serializeExpiry]) + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_NEW_OR_EXISTING, + NONE, + expiry + ); + } else { + const isNew = encodeInt8(1); + const address = Buffer.concat([isNew, Buffer.from(addressOrExpiry as string, "hex")]) + response = await this.sendToDevice( + INS.SIGN_CREDENTIAL_DEPLOYMENT, + P1_NEW_OR_EXISTING, + NONE, + address + ); + } + + if (response.length === 1) throw new Error("User has declined."); + + return { + signature: response.toString("hex"), + }; + } + async signUpdateCredentials(txn, path: string): Promise<{ signature: string[] }> { const { payloadHeaderKindAndIndexLength, credentialIndex, numberOfVerificationKeys, keyIndexAndSchemeAndVerificationKey, thresholdAndRegIdAndIPIdentity, encIdCredPubShareAndKey, validToAndCreatedAtAndAttributesLength, tag, valueLength, value, proofLength, proofs, credentialIdCount, credentialIds, threshold } = serializeUpdateCredentials(txn, path); diff --git a/src/serialization.ts b/src/serialization.ts index 29386f6..1f087f1 100644 --- a/src/serialization.ts +++ b/src/serialization.ts @@ -3,6 +3,7 @@ import { encodeDataBlob, encodeInt8, encodeWord16, encodeWord64, serializeAccoun import { DataBlob } from "@concordium/common-sdk/lib/types/DataBlob"; import { Buffer as NodeBuffer } from 'buffer/index'; import { AccountAddress } from "@concordium/web-sdk"; +import { serializeCredentialDeploymentInfo } from "@concordium/common-sdk/lib/serialization"; const MAX_CHUNK_SIZE = 255; const MAX_SCHEDULE_CHUNK_SIZE = 15; @@ -344,6 +345,53 @@ export const serializeUpdateContract = (txn: any, path: string): { payloads: Buf return serializeTransaction(txn, path); }; +export const serializeCredentialDeployment = (txn: any, path: string): { payloadDerivationPath: Buffer, numberOfVerificationKeys: Buffer, keyIndexAndSchemeAndVerificationKey: Buffer, thresholdAndRegIdAndIPIdentity: Buffer, encIdCredPubShareAndKey: Buffer, validToAndCreatedAtAndAttributesLength: Buffer, attributesLength: Buffer, tag: Buffer[], valueLength: Buffer[], value: Buffer[], proofLength: Buffer, proofs: Buffer } => { + let offset = 0; + const txSerialized = serializeCredentialDeploymentInfo(txn); + const payloadDerivationPath = pathToBuffer(path); + let numberOfVerificationKeys: Buffer = Buffer.alloc(0); + let keyIndexAndSchemeAndVerificationKey: Buffer = Buffer.alloc(0); + let thresholdAndRegIdAndIPIdentity: Buffer = Buffer.alloc(0); + let encIdCredPubShareAndKey: Buffer = Buffer.alloc(0); + let validToAndCreatedAtAndAttributesLength: Buffer = Buffer.alloc(0); + let attributesLength: Buffer = Buffer.alloc(0); + let tag: Buffer[] = []; + let valueLength: Buffer[] = []; + let value: Buffer[] = []; + let proofLength: Buffer = Buffer.alloc(0); + let proofs: Buffer = Buffer.alloc(0); + + numberOfVerificationKeys = Buffer.from(txSerialized.subarray(offset, offset + INDEX_LENGTH)); + offset += INDEX_LENGTH; + keyIndexAndSchemeAndVerificationKey = Buffer.from(txSerialized.subarray(offset, offset + 2 * ONE_OCTET_LENGTH + KEY_LENGTH)); + offset += 2 * ONE_OCTET_LENGTH + KEY_LENGTH; + thresholdAndRegIdAndIPIdentity = Buffer.from(txSerialized.subarray(offset, offset + 2 * ONE_OCTET_LENGTH + REG_ID_LENGTH + IP_IDENTITY_LENGTH + AR_DATA_LENGTH)); + offset += 2 * ONE_OCTET_LENGTH + REG_ID_LENGTH + IP_IDENTITY_LENGTH + AR_DATA_LENGTH; + encIdCredPubShareAndKey = Buffer.from(txSerialized.subarray(offset, offset + 4 * ONE_OCTET_LENGTH + ID_CRED_PUB_SHARE_LENGTH)); + offset += 4 * ONE_OCTET_LENGTH + ID_CRED_PUB_SHARE_LENGTH; + validToAndCreatedAtAndAttributesLength = Buffer.from(txSerialized.subarray(offset, offset + ATTRIBUTES_LENGTH + VALID_TO_LENGTH + CREATED_AT_LENGTH)); + offset += ATTRIBUTES_LENGTH + VALID_TO_LENGTH + CREATED_AT_LENGTH; + attributesLength = validToAndCreatedAtAndAttributesLength.subarray(-ATTRIBUTES_LENGTH); + tag = []; + valueLength = []; + value = []; + for (let j = 0; j < attributesLength.readUInt16BE(0); j++) { + tag.push(Buffer.from(txSerialized.subarray(offset, offset + TAG_LENGTH))); + offset += TAG_LENGTH; + valueLength.push(Buffer.from(txSerialized.subarray(offset, offset + VALUE_LENGTH))); + offset += VALUE_LENGTH; + value.push(Buffer.from(txSerialized.subarray(offset, offset + valueLength[j].readUInt8(0)))); + offset += valueLength[j].readUInt8(0); + } + + proofLength = Buffer.from(txSerialized.subarray(offset, offset + PROOF_LENGTH_LENGTH)); + offset += PROOF_LENGTH_LENGTH; + proofs = Buffer.from(txSerialized.subarray(offset, offset + proofLength.readUInt32BE(0))); + offset += proofLength.readUInt32BE(0); + + return { payloadDerivationPath, numberOfVerificationKeys, keyIndexAndSchemeAndVerificationKey, thresholdAndRegIdAndIPIdentity, encIdCredPubShareAndKey, validToAndCreatedAtAndAttributesLength, attributesLength, tag, valueLength, value, proofLength, proofs }; +}; + export const serializeUpdateCredentials = (txn: any, path: string): { payloadHeaderKindAndIndexLength: Buffer[], credentialIndex: Buffer[], numberOfVerificationKeys: Buffer[], keyIndexAndSchemeAndVerificationKey: Buffer[], thresholdAndRegIdAndIPIdentity: Buffer[], encIdCredPubShareAndKey: Buffer[], validToAndCreatedAtAndAttributesLength: Buffer[], attributesLength: Buffer[], tag: Buffer[][], valueLength: Buffer[][], value: Buffer[][], proofLength: Buffer[], proofs: Buffer[], credentialIdCount: Buffer, credentialIds: Buffer[], threshold: Buffer } => { let offset = 0; const txSerialized = serializeAccountTransaction(txn); @@ -358,7 +406,7 @@ export const serializeUpdateCredentials = (txn: any, path: string): { payloadHea let encIdCredPubShareAndKey: Buffer[] = []; let validToAndCreatedAtAndAttributesLength: Buffer[] = []; let attributesLength: Buffer[] = []; - let tag: Buffer[][] = [[]]; + let tag: Buffer[][] = [[]]; let valueLength: Buffer[][] = [[]]; let value: Buffer[][] = [[]]; let proofLength: Buffer[] = []; From c25f5c4bce32c76f722def067b00058e6da35352 Mon Sep 17 00:00:00 2001 From: GuilaneDen <83951892+GuilaneDen@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:56:07 +0100 Subject: [PATCH 7/9] feat: implemented exportPrivateKey --- src/Concordium.ts | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/Concordium.ts b/src/Concordium.ts index 7b1d1f7..190eb9d 100644 --- a/src/Concordium.ts +++ b/src/Concordium.ts @@ -19,6 +19,23 @@ import { } from "./serialization"; import { encodeInt32, encodeInt8, encodeWord64 } from "./utils"; +export enum ExportType { + PRF_KEY_SEED = 1, + PRF_KEY = 2, +} +export enum Mode { + NO_DISPLAY = 0, + DISPLAY = 1, + EXPORT_CRED_ID = 2 +} + +interface IExportPrivateKeyData { + identity: number; + identityProvider: number; +} + +const PRIVATE_KEY_LENGTH = 32; + const LEDGER_CLA = 0xe0; // FOR GET VERSION AND APP NAME @@ -81,6 +98,7 @@ const INS = { SIGN_TRANSFER: 0x02, SIGN_TRANSFER_SCHEDULE: 0x03, SIGN_CREDENTIAL_DEPLOYMENT: 0x04, + EXPORT_PRIVATE_KEY: 0x05, SIGN_TRANSFER_TO_PUBLIC: 0x12, SIGN_CONFIGURE_DELEGATION: 0x17, SIGN_CONFIGURE_BAKER: 0x18, @@ -228,6 +246,45 @@ export default class Concordium { }; } + /** + * Get Concordium address (public key) for a BIP32 path. + * + * @param exportType either export PRF_KEY_SEED or PRF_KEY + * @param mode either DISPLAY, NO_DISPLAY or EXPORT_CRED_ID + * @param isLegacy flag to indicate if the legacy mode is used + * @returns an object with the address field + * + */ + async exportPrivateKey(data: IExportPrivateKeyData, exportType: ExportType, mode: Mode, isLegacy: boolean): Promise<{ privateKey: string, credentialId?: string }> { + let payload = Buffer.alloc(0); + const isLegacyEncoded = isLegacy ? encodeInt8(0) : encodeInt8(1); + const identityEncoded = encodeInt32(data.identity); + payload = Buffer.concat([payload, isLegacyEncoded, identityEncoded]); + + if (!isLegacy ) { + const identityProviderEncoded = encodeInt32(data.identityProvider); + payload = Buffer.concat([payload, identityProviderEncoded]); + } + + const exportedPrivateKey = await this.sendToDevice( + INS.EXPORT_PRIVATE_KEY, + mode, + exportType, + payload + ); + + if (mode === Mode.EXPORT_CRED_ID) { + return { + privateKey: exportedPrivateKey.subarray(0, PRIVATE_KEY_LENGTH).toString("hex"), + credentialId: exportedPrivateKey.subarray(PRIVATE_KEY_LENGTH).toString("hex"), + }; + } + + return { + privateKey: exportedPrivateKey.toString("hex"), + }; + } + /** * Signs a Concordium transaction using the specified account index. * @param txn - The transaction to sign. From 7980bec5195455ffb997827f1c3afd8283a4110e Mon Sep 17 00:00:00 2001 From: GuilaneDen <83951892+GuilaneDen@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:13:02 +0100 Subject: [PATCH 8/9] fix: replaced ledgerhq with blooo-io --- package-lock.json | 4 ++-- package.json | 2 +- src/Concordium.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7caa0c4..bdc2a6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@ledgerhq/hw-app-concordium", + "name": "@blooo-io/hw-app-concordium", "version": "6.29.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@ledgerhq/hw-app-concordium", + "name": "@blooo-io/hw-app-concordium", "version": "6.29.4", "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index b01dace..3856d36 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@ledgerhq/hw-app-concordium", + "name": "@blooo-io/hw-app-concordium", "type": "module", "version": "6.29.4", "description": "Ledger Hardware Wallet Concordium Application API", diff --git a/src/Concordium.ts b/src/Concordium.ts index 190eb9d..aaca075 100644 --- a/src/Concordium.ts +++ b/src/Concordium.ts @@ -119,7 +119,7 @@ const INS = { * @param scrambleKey a scramble key * * @example - * import Concordium from "@ledgerhq/hw-app-concordium"; + * import Concordium from "@blooo-io/hw-app-concordium"; * const Concordium = new Concordium(transport); */ export default class Concordium { From e2ab09e2fc31f260a532e0b29701a7bee04fd78d Mon Sep 17 00:00:00 2001 From: GuilaneDen <83951892+GuilaneDen@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:14:21 +0100 Subject: [PATCH 9/9] fix: reinitialized project version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3856d36..a0d32c1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@blooo-io/hw-app-concordium", "type": "module", - "version": "6.29.4", + "version": "1.0.0", "description": "Ledger Hardware Wallet Concordium Application API", "keywords": [ "Ledger",