UNPKG

@blooo/hw-app-concordium

Version:

Ledger Hardware Wallet Concordium Application API

1,129 lines (990 loc) 32.4 kB
import Transport from "@ledgerhq/hw-transport"; import { StatusCodes } from "@ledgerhq/errors"; import { pathToBuffer, serializeConfigureDelegation, serializeSimpleTransfer, serializeSimpleTransferWithMemo, serializeTransferWithSchedule, serializeConfigureBaker, serializeTransferWithScheduleAndMemo, serializeRegisterData, serializeTransferToPublic, serializeDeployModule, serializeInitContract, serializeUpdateContract, serializeTransactionPayloads, serializeUpdateCredentials, serializeCredentialDeployment, serializePublicInfoForIp, serializePltTransaction } from "./serialization"; import { encodeInt32, encodeInt8, encodeWord64 } from "./utils"; import { Mode, ExportType, ExportTypeNew, IExportPrivateKeyData, ISimpleTransferTransaction, ISimpleTransferWithMemoTransaction, ISimpleTransferWithScheduleTransaction, ISimpleTransferWithScheduleAndMemoTransaction, IConfigureDelegationTransaction, IRegisterDataTransaction, ITransferToPublicTransaction, IDeployModuleTransaction, IInitContractTransaction, IUpdateContractTransaction, IPublicInfoForIpTransaction, ICredentialDeploymentTransaction, IUpdateCredentialsTransaction, IConfigureBakerTransaction, IPLTTransaction } from "./type"; export type { ExportType, ExportTypeNew, IPLTTransaction }; const PRIVATE_KEY_LENGTH = 32; const PUBLIC_KEY_LENGTH = 32; const LEDGER_CLA = 0xe0; const NONE = 0x00; // FOR VERIFY ADRESS const P1_LEGACY_VERIFY_ADDRESS = 0x00; const P1_VERIFY_ADDRESS = 0x01; // FOR GET PUBLIC KEY const P1_NON_CONFIRM = 0x00; 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_SUSPENDED = 0x06; 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; // Deploy Module const P1_SOURCE = 0x01; const P1_NAME = 0x01; const P1_PARAM = 0x02; // 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 // FOR EXPORT PRIVATE KEY NEW const P1_IDENTITY_CREDENTIAL_CREATION = 0x00; const P1_ACCOUNT_CREATION = 0x01; const P1_ID_RECOVERY = 0x02; const P1_ACCOUNT_CREDENTIAL_DISCOVERY = 0x03; const P1_CREATION_OF_ZK_PROOF = 0x04; const INS = { VERIFY_ADDRESS: 0x00, GET_PUBLIC_KEY: 0x01, SIGN_TRANSFER: 0x02, SIGN_TRANSFER_SCHEDULE: 0x03, SIGN_CREDENTIAL_DEPLOYMENT: 0x04, EXPORT_PRIVATE_KEY_LEGACY: 0x05, SIGN_DEPLOY_MODULE: 0x06, SIGN_INIT_CONTRACT: 0x07, SIGN_UPDATE_CONTRACT: 0x08, SIGN_TRANSFER_TO_PUBLIC: 0x12, SIGN_CONFIGURE_DELEGATION: 0x17, SIGN_CONFIGURE_BAKER: 0x18, SIGN_PUBLIC_INFO_FOR_IP: 0x20, GET_APP_NAME: 0x21, SIGN_UPDATE_CREDENTIALS: 0x31, SIGN_TRANSFER_MEMO: 0x32, SIGN_TRANSFER_SCHEDULE_AND_MEMO: 0x34, SIGN_REGISTER_DATA: 0x35, EXPORT_PRIVATE_KEY_NEW: 0x37, SIGN_PLT_TRANSACTION: 0x38, }; /** * Concordium API * * @param transport a transport for sending commands to a device * @param scrambleKey a scramble key * * @example * import Concordium from "@blooo/hw-app-concordium"; * const Concordium = new Concordium(transport); */ export default class Concordium { private transport: Transport; constructor( transport: Transport, scrambleKey = "concordium_default_scramble_key" ) { this.transport = transport; this.transport.decorateAppAPIMethods( this, [ "getAddress", "verifyAddress", "verifyAddressLegacy", "getPublicKey", "exportPrivateKey", "exportPrivateKeyNew", "signTransfer", "signTransferWithMemo", "signTransferWithSchedule", "signTransferWithScheduleAndMemo", "signConfigureDelegation", "signConfigureBaker", "signRegisterData", "signTransferToPublic", "signDeployModule", "signInitContract", "signUpdateContract", "signPublicInfoForIp", "signUpdateCredentials", "signCredentialDeployment", "signPLT", ], scrambleKey ); } /** * Verify address. * * @param isLegacy - Flag to indicate if the legacy mode is used. * @param id - The identity number. * @param cred - The credential number. * @param idp - Mandatory if isLegacy is false. The identity provider number. * @returns A promise that resolves to an object containing the status. * * @example * concordium.verifyAddress(12,12,12).then(r => r.status) */ async verifyAddress(isLegacy: boolean, id: number, cred: number, idp?: number): Promise<{ status: string }> { try { const idEncoded = encodeInt32(id); let payload = Buffer.from(idEncoded); if (!isLegacy) { const idpEncoded = encodeInt32(idp); payload = Buffer.concat([payload, idpEncoded]); } const credEncoded = encodeInt32(cred); payload = Buffer.concat([payload, credEncoded]); await this.sendToDevice( INS.VERIFY_ADDRESS, isLegacy ? P1_LEGACY_VERIFY_ADDRESS : P1_VERIFY_ADDRESS, NONE, payload ); return { status: "success" }; } catch (error) { return { status: "failed" }; }; } /** * Get Concordium address (public key) for a BIP32 path. * * @param path - A BIP32 path. * @param display - Flag to show display. * @param signedKey - Flag to sign key. * @returns A promise that resolves to an object with the public key and optionally the signed public key. * * @example * concordium.getPublicKey("1105'/0'/0'/0/0/0/0/", true, false) */ 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_NON_CONFIRM : P1_CONFIRM, signedKey ? P2_SIGNED_KEY : NONE, pathBuffer ); if (signedKey) { return { publicKey: publicKeyBuffer.subarray(0, PUBLIC_KEY_LENGTH).toString("hex"), signedPublicKey: publicKeyBuffer.subarray(PUBLIC_KEY_LENGTH).toString("hex"), }; } return { publicKey: publicKeyBuffer.subarray(0, PUBLIC_KEY_LENGTH).toString("hex"), }; } /** * Export a private key. * * @param data - The data required for exporting the private key. * @param exportType - The type of export, either PRF_KEY_SEED or PRF_KEY. * @param mode - The mode, either DISPLAY, NO_DISPLAY, or EXPORT_CRED_ID. * @returns A promise that resolves to an object with the private key and optionally the credential ID. */ async exportPrivateKeyLegacy(data: IExportPrivateKeyData, exportType: ExportType, mode: Mode): Promise<{ privateKey: string, credentialId?: string }> { const identityEncoded = encodeInt32(data.identity); const payload = Buffer.from(identityEncoded); const exportedPrivateKey = await this.sendToDevice( INS.EXPORT_PRIVATE_KEY_LEGACY, 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"), }; } /** * Export a private key. * * @param exportType - The type of export: identity_credential_creation, account_creation, id_recovery, account_credential_discovery, or creation_of_zk_proof. * @param identityIndex - The identity index. * @param idpIndex - The identity provider index. * @param accountIndex - The account index (optional, only used for some export types). * @returns A promise that resolves to an object with the private key and optionally the credential ID. */ async exportPrivateKeyNew( exportType: ExportTypeNew, identityIndex: number, idpIndex: number, accountIndex?: number ): Promise<{ privateKey: string }> { let p1: number; // Map export type to P1 value switch (exportType) { case "identity_credential_creation": p1 = P1_IDENTITY_CREDENTIAL_CREATION; break; case "account_creation": p1 = P1_ACCOUNT_CREATION; break; case "id_recovery": p1 = P1_ID_RECOVERY; break; case "account_credential_discovery": p1 = P1_ACCOUNT_CREDENTIAL_DISCOVERY; break; case "creation_of_zk_proof": p1 = P1_CREATION_OF_ZK_PROOF; break; default: throw new Error(`Invalid export type: ${exportType}`); } // Construct data payload: idp_index first, then identity_index, then optionally account_index const idpIndexEncoded = encodeInt32(idpIndex); const identityIndexEncoded = encodeInt32(identityIndex); let payload = Buffer.concat([idpIndexEncoded, identityIndexEncoded]); if (accountIndex !== undefined) { const accountIndexEncoded = encodeInt32(accountIndex); payload = Buffer.concat([payload, accountIndexEncoded]); } const exportedPrivateKey = await this.sendToDevice( INS.EXPORT_PRIVATE_KEY_NEW, p1, NONE, payload ); return { privateKey: exportedPrivateKey.toString("hex"), }; } /** * Signs a PLT transaction. * * @param txn - The PLT transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signPLT(txn: IPLTTransaction, path: string): Promise<{ signature: string }> { // Type checking to provide clearer error messages if (typeof path !== 'string') { throw new Error('signPLT: path parameter must be a string. Note: parameter order changed - use signPLT(transaction, path)'); } if (!txn || typeof txn !== 'object') { throw new Error('signPLT: txn parameter must be a PLT transaction object'); } const payloads = serializePltTransaction(txn, path); let response: Buffer; // Send all chunks except the last one with P2_MORE for (let i = 0; i < payloads.length - 1; i++) { await this.sendToDevice( INS.SIGN_PLT_TRANSACTION, i, // p1 = index P2_MORE, payloads[i] ); } // Send the last chunk with P2_LAST and get the response response = await this.sendToDevice( INS.SIGN_PLT_TRANSACTION, payloads.length - 1, // p1 = index of last chunk P2_LAST, payloads[payloads.length - 1] ); if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), }; } /** * Signs a Concordium transaction using the specified account index. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. * @throws Error if the user declines the transaction. * * @example * concordium.signTransfer(txn).then(r => r.signature) */ async signTransfer(txn: ISimpleTransferTransaction, path: string): Promise<{ signature: string }> { const { payloads } = serializeSimpleTransfer(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, 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"), }; } /** * Signs a simple transfer with a memo. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signTransferWithMemo(txn: ISimpleTransferWithMemoTransaction, path: string): Promise<{ signature: string[] }> { const { payloadHeaderAddressMemoLength, payloadsMemo, payloadsAmount } = serializeSimpleTransferWithMemo(txn, path); let response; await this.sendToDevice( INS.SIGN_TRANSFER_MEMO, P1_INITIAL_WITH_MEMO, NONE, payloadHeaderAddressMemoLength[0] ); 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"), }; } /** * Signs a transfer with a schedule. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signTransferWithSchedule(txn: ISimpleTransferWithScheduleTransaction, path: string): Promise<{ signature: string[] }> { const { payloadHeaderAddressScheduleLength, payloadsSchedule } = serializeTransferWithSchedule(txn, path); let response; await this.sendToDevice( INS.SIGN_TRANSFER_SCHEDULE, P1_INITIAL_PACKET, NONE, payloadHeaderAddressScheduleLength[0] ); for (const schedule of payloadsSchedule) { response = await this.sendToDevice( INS.SIGN_TRANSFER_SCHEDULE, P1_SCHEDULED_TRANSFER_PAIRS, NONE, schedule ); } if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), }; } /** * Signs a transfer with a schedule and a memo. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signTransferWithScheduleAndMemo(txn: ISimpleTransferWithScheduleAndMemoTransaction, path: string): Promise<{ signature: string[] }> { const { payloadHeaderAddressScheduleLengthAndMemoLength, payloadMemo, payloadsSchedule } = serializeTransferWithScheduleAndMemo(txn, path); let response; await this.sendToDevice( INS.SIGN_TRANSFER_SCHEDULE_AND_MEMO, P1_INITIAL_WITH_MEMO_SCHEDULE, NONE, payloadHeaderAddressScheduleLengthAndMemoLength[0] ); await this.sendToDevice( INS.SIGN_TRANSFER_SCHEDULE_AND_MEMO, P1_MEMO_SCHEDULE, NONE, payloadMemo[0] ); for (const schedule of payloadsSchedule) { response = await this.sendToDevice( INS.SIGN_TRANSFER_SCHEDULE_AND_MEMO, P1_SCHEDULED_TRANSFER_PAIRS, NONE, schedule ); } if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), }; } /** * Signs a configure delegation transaction. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signConfigureDelegation(txn: IConfigureDelegationTransaction, path: string): Promise<{ signature: string[] }> { 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_CONFIGURE_DELEGATION, 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"), }; } /** * Signs a configure baker transaction. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signConfigureBaker(txn: IConfigureBakerTransaction, path: string): Promise<{ signature: string[] }> { const { payloadHeaderKindAndBitmap, payloadFirstBatch, payloadAggregationKeys, payloadUrlLength, payloadURL, payloadCommissionFee, payloadSuspended } = serializeConfigureBaker(txn, path); let response; await this.sendToDevice( INS.SIGN_CONFIGURE_BAKER, P1_INITIAL_PACKET, NONE, payloadHeaderKindAndBitmap ); await this.sendToDevice( INS.SIGN_CONFIGURE_BAKER, P1_FIRST_BATCH, NONE, payloadFirstBatch ); await this.sendToDevice( INS.SIGN_CONFIGURE_BAKER, P1_AGGREGATION_KEY, NONE, payloadAggregationKeys ); await this.sendToDevice( INS.SIGN_CONFIGURE_BAKER, P1_URL_LENGTH, NONE, payloadUrlLength ); await this.sendToDevice( INS.SIGN_CONFIGURE_BAKER, P1_URL, NONE, payloadURL ); await this.sendToDevice( INS.SIGN_CONFIGURE_BAKER, P1_COMMISSION_FEE, NONE, payloadCommissionFee ); response = await this.sendToDevice( INS.SIGN_CONFIGURE_BAKER, P1_SUSPENDED, NONE, payloadSuspended ); if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), }; } /** * Signs a register data transaction. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signRegisterData(txn: IRegisterDataTransaction, path: string): Promise<{ signature: string[] }> { const { payloadHeader, payloadsData } = serializeRegisterData(txn, path); let response; await this.sendToDevice( INS.SIGN_REGISTER_DATA, P1_INITIAL_PACKET, NONE, payloadHeader[0] ); for (const data of payloadsData) { response = await this.sendToDevice( INS.SIGN_REGISTER_DATA, P1_DATA, NONE, data ); } if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), }; } /** * Signs a transfer to public transaction. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signTransferToPublic(txn: ITransferToPublicTransaction, path: string): Promise<{ signature: string[] }> { const { payloadHeader, payloadsAmountRecipientAndProofsLength, payloadsProofs } = serializeTransferToPublic(txn, path); let response; await this.sendToDevice( INS.SIGN_TRANSFER_TO_PUBLIC, P1_INITIAL_PACKET, NONE, payloadHeader[0] ); await this.sendToDevice( INS.SIGN_TRANSFER_TO_PUBLIC, P1_REMAINING_AMOUNT, NONE, payloadsAmountRecipientAndProofsLength[0] ); for (const proof of payloadsProofs) { response = await this.sendToDevice( INS.SIGN_TRANSFER_TO_PUBLIC, P1_PROOF, NONE, proof ); } if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), }; } /** * Signs a deploy module transaction. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signDeployModule(txn: IDeployModuleTransaction, path: string): Promise<{ signature: string[] }> { const { payloadsHeaderAndVersion, payloadSource } = serializeDeployModule(txn, path); let response; await this.sendToDevice( INS.SIGN_DEPLOY_MODULE, P1_INITIAL_PACKET, P2_LAST, payloadsHeaderAndVersion[0] ); response = await this.sendToDevice( INS.SIGN_DEPLOY_MODULE, P1_SOURCE, P2_LAST, payloadSource ); if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), }; } /** * Signs an init contract transaction. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signInitContract(txn: IInitContractTransaction, path: string): Promise<{ signature: string[] }> { const { payloadsHeaderAndData, payloadsName, payloadsParam } = serializeInitContract(txn, path); let response; await this.sendToDevice( INS.SIGN_INIT_CONTRACT, P1_INITIAL_PACKET, NONE, payloadsHeaderAndData[0] ); for (const nameChunk of payloadsName) { await this.sendToDevice( INS.SIGN_INIT_CONTRACT, P1_NAME, NONE, nameChunk ); } for (const paramChunk of payloadsParam) { response = await this.sendToDevice( INS.SIGN_INIT_CONTRACT, P1_PARAM, NONE, paramChunk ); } if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), }; } /** * Signs an update contract transaction. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signUpdateContract(txn: IUpdateContractTransaction, path: string): Promise<{ signature: string[] }> { const { payloadsHeaderAndData, payloadsName, payloadsParam } = serializeUpdateContract(txn, path); let response; await this.sendToDevice( INS.SIGN_UPDATE_CONTRACT, P1_INITIAL_PACKET, NONE, payloadsHeaderAndData[0] ); for (const nameChunk of payloadsName) { await this.sendToDevice( INS.SIGN_UPDATE_CONTRACT, P1_NAME, NONE, nameChunk ); } for (const paramChunk of payloadsParam) { response = await this.sendToDevice( INS.SIGN_UPDATE_CONTRACT, P1_PARAM, NONE, paramChunk ); } if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), }; } /** * Signs public info for IP transaction. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signPublicInfoForIp(txn: IPublicInfoForIpTransaction, path: string): Promise<{ signature: string[] }> { const { payloadIdCredPubAndRegIdAndKeysLenght, payloadKeys, payloadThreshold } = serializePublicInfoForIp(txn, path); let response; await this.sendToDevice( INS.SIGN_PUBLIC_INFO_FOR_IP, P1_INITIAL_PACKET, NONE, payloadIdCredPubAndRegIdAndKeysLenght ); for (const key of payloadKeys) { await this.sendToDevice( INS.SIGN_PUBLIC_INFO_FOR_IP, P1_VERIFICATION_KEY, NONE, key ); } response = await this.sendToDevice( INS.SIGN_PUBLIC_INFO_FOR_IP, P1_SIGNATURE_THRESHOLD, NONE, payloadThreshold ); if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), }; } /** * Signs a credential deployment transaction. * * @param txn - The transaction to sign. * @param isNew - Flag indicating if it's a new credential. * @param addressOrExpiry - The address or expiry date. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signCredentialDeployment(txn: ICredentialDeploymentTransaction, 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; await this.sendToDevice( INS.SIGN_CREDENTIAL_DEPLOYMENT, P1_INITIAL_PACKET, NONE, payloadDerivationPath ); await this.sendToDevice( INS.SIGN_CREDENTIAL_DEPLOYMENT, P1_VERIFICATION_KEY_LENGTH, NONE, numberOfVerificationKeys ); await this.sendToDevice( INS.SIGN_CREDENTIAL_DEPLOYMENT, P1_VERIFICATION_KEY, NONE, keyIndexAndSchemeAndVerificationKey ); await this.sendToDevice( INS.SIGN_CREDENTIAL_DEPLOYMENT, P1_SIGNATURE_THRESHOLD, NONE, thresholdAndRegIdAndIPIdentity ); await this.sendToDevice( INS.SIGN_CREDENTIAL_DEPLOYMENT, P1_AR_IDENTITY, NONE, encIdCredPubShareAndKey ); 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]]) await this.sendToDevice( INS.SIGN_CREDENTIAL_DEPLOYMENT, P1_ATTRIBUTE_TAG, NONE, tagAndValueLength ); await this.sendToDevice( INS.SIGN_CREDENTIAL_DEPLOYMENT, P1_ATTRIBUTE_VALUE, NONE, value[i] ); } await this.sendToDevice( INS.SIGN_CREDENTIAL_DEPLOYMENT, P1_LENGTH_OF_PROOFS, NONE, proofLength ); const proofPayload = serializeTransactionPayloads(proofs); for (const proof of proofPayload) { await this.sendToDevice( INS.SIGN_CREDENTIAL_DEPLOYMENT, P1_PROOFS, NONE, proof ); } 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"), }; } /** * Signs an update credentials transaction. * * @param txn - The transaction to sign. * @param path - The derivation path to use for signing. * @returns A promise that resolves to an object containing the signature. */ async signUpdateCredentials(txn: IUpdateCredentialsTransaction, 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; await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, NONE, P2_CREDENTIAL_INITIAL, payloadHeaderKindAndIndexLength[0] ); for (let i = 0; i < txn.payload.newCredentials.length; i++) { await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, NONE, P2_CREDENTIAL_CREDENTIAL_INDEX, credentialIndex[i] ); await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, P1_VERIFICATION_KEY_LENGTH, P2_CREDENTIAL_CREDENTIAL, numberOfVerificationKeys[i] ); await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, P1_VERIFICATION_KEY, P2_CREDENTIAL_CREDENTIAL, keyIndexAndSchemeAndVerificationKey[i] ); await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, P1_SIGNATURE_THRESHOLD, P2_CREDENTIAL_CREDENTIAL, thresholdAndRegIdAndIPIdentity[i] ); await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, P1_AR_IDENTITY, P2_CREDENTIAL_CREDENTIAL, encIdCredPubShareAndKey[i] ); 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]]) await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, P1_ATTRIBUTE_TAG, P2_CREDENTIAL_CREDENTIAL, tagAndValueLength ); await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, P1_ATTRIBUTE_VALUE, P2_CREDENTIAL_CREDENTIAL, value[i][j] ); } await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, P1_LENGTH_OF_PROOFS, P2_CREDENTIAL_CREDENTIAL, proofLength[i] ); const proofPayload = serializeTransactionPayloads(proofs[i]); for (const proof of proofPayload) { await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, P1_PROOFS, P2_CREDENTIAL_CREDENTIAL, proof ); } } await this.sendToDevice( INS.SIGN_UPDATE_CREDENTIALS, NONE, P2_CREDENTIAL_ID_COUNT, credentialIdCount ); for (let i = 0; i < txn.payload.removeCredentialIds.length; i++) { 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"), }; } /** * Sends a command to the device. * * @param instruction - The instruction code. * @param p1 - The first parameter. * @param p2 - The second parameter. * @param payload - The payload to send. * @returns A promise that resolves to the device's response. */ private async sendToDevice( instruction: number, p1: number, p2: number = 0x00, payload: Buffer ) { const acceptStatusList = [StatusCodes.OK]; const reply = await this.transport.send( LEDGER_CLA, instruction, p1, p2, payload, acceptStatusList ); this.throwOnFailure(reply); return reply.subarray(0, reply.length - 2); } /** * Throws an error if the device response indicates a failure. * * @param reply - The device's response. */ private throwOnFailure(reply: Buffer) { // transport makes sure reply has a valid length const status = reply.readUInt16BE(reply.length - 2); switch (status) { default: return; } } }