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/.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..bdc2a6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@ledgerhq/hw-app-algorand", + "name": "@blooo-io/hw-app-concordium", "version": "6.29.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@ledgerhq/hw-app-algorand", + "name": "@blooo-io/hw-app-concordium", "version": "6.29.4", "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index dc74751..a0d32c1 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,34 @@ { - "name": "@ledgerhq/hw-app-algorand", + "name": "@blooo-io/hw-app-concordium", "type": "module", - "version": "6.29.4", - "description": "Ledger Hardware Wallet Algorand Application API", + "version": "1.0.0", + "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 b9a1e03..aaca075 100644 --- a/src/Concordium.ts +++ b/src/Concordium.ts @@ -6,9 +6,35 @@ import { serializeSimpleTransfer, serializeSimpleTransferWithMemo, serializeTransferWithSchedule, + serializeConfigureBaker, + serializeTransferWithScheduleAndMemo, + serializeRegisterData, + serializeTransferToPublic, + serializeDeployModule, + serializeInitContract, + serializeUpdateContract, + serializeTransactionPayloads, + serializeUpdateCredentials, + serializeCredentialDeployment } from "./serialization"; -import BigNumber from "bignumber.js"; -import { encodeInt32 } from "./utils"; +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; @@ -25,21 +51,67 @@ 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; +const P1_MEMO_SCHEDULE = 0x03; +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; + +// 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, VERIFY_ADDRESS: 0x00, GET_PUBLIC_KEY: 0x01, - GET_VERSION: 0x03, - GET_APP_NAME: 0x04, - SIGN_TX: 0x06, + 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, + 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, }; -const concordium_path = "44'/919'/0'/0/0/0"; -const concordium_legacy_path = "1105'/0'/0'/0/"; - /** * Concordium API * @@ -47,7 +119,7 @@ const concordium_legacy_path = "1105'/0'/0'/0/"; * @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 { @@ -70,25 +142,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. @@ -150,23 +222,69 @@ 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( INS.GET_PUBLIC_KEY, - display ? P1_CONFIRM : P1_NON_CONFIRM, + display ? P1_NON_CONFIRM : P1_CONFIRM, signedKey ? P2_SIGNED_KEY : NONE, pathBuffer ); 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"), }; } + /** + * 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. @@ -185,7 +303,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] @@ -202,17 +320,115 @@ 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 { 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[] }> { + + + 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 < payloadsSchedule.length; i++) { + response = await this.sendToDevice( + INS.SIGN_TRANSFER_SCHEDULE_AND_MEMO, + P1_SCHEDULED_TRANSFER_PAIRS, + NONE, + payloadsSchedule[i] + ); + } + + if (response.length === 1) throw new Error("User has declined."); + + return { + signature: response.toString("hex"), + }; + } + + async signConfigureDelegation(txn, path: string): Promise<{ signature: string[] }> { - const { payloads } = serializeSimpleTransferWithMemo(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_TX, + INS.SIGN_CONFIGURE_DELEGATION, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -220,25 +436,136 @@ 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 signTransferWithSchedule(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signConfigureBaker(txn, path: string): Promise<{ signature: string[] }> { + + const { payloadHeaderKindAndBitmap, payloadFirstBatch, payloadAggregationKeys, payloadUrlLength, payloadURL, payloadCommissionFee } = serializeConfigureBaker(txn, path); + + let response; + + 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."); + + return { + signature: response.toString("hex"), + }; + } + + async signRegisterData(txn, path: string): Promise<{ signature: string[] }> { + + const { payloadHeader, payloadsData } = serializeRegisterData(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[] }> { - const { payloads } = serializeTransferWithSchedule(txn, path); + 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_TX, + INS.SIGN_DEPLOY_MODULE, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -247,25 +574,44 @@ 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 signConfigureDelegation(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signInitContract(txn, path: string): Promise<{ signature: string[] }> { + const { payloads } = serializeInitContract(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_INIT_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 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_TX, + INS.SIGN_UPDATE_CONTRACT, P1_FIRST_CHUNK + i, lastChunk ? P2_LAST : P2_MORE, payloads[i] @@ -274,12 +620,221 @@ 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 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); + + 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 432863e..1f087f1 100644 --- a/src/serialization.ts +++ b/src/serialization.ts @@ -1,9 +1,40 @@ -import BigNumber from "bignumber.js"; 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/'; +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; +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; +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 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); @@ -36,18 +67,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; +}; + + +export const serializeTransactionPayloads = (rawTx: Buffer): Buffer[] => { + let offset = 0; + const payloads: Buffer[] = []; while (offset !== rawTx.length) { const first = offset === 0; let chunkSize = @@ -67,9 +124,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 }; } @@ -77,20 +135,323 @@ 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.memo = new DataBlob(memoBuffer); + 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[] } => { return serializeTransaction(txn, path); }; -export const serializeTransferWithSchedule = (txn: any, path: string): { payloads: Buffer[] } => { +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 }; +}; + + +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); + + 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[] } => { + // 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); +}; + +export const serializeUpdateContract = (txn: any, path: string): { payloads: Buffer[] } => { 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); + 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 }; +}; diff --git a/src/utils.ts b/src/utils.ts index 1b08f9b..191f534 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,42 @@ 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 { + 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]); +} + /** * 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 @@ -103,7 +160,7 @@ function serializeSchedule(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); @@ -130,11 +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);