import { lastValueFrom } from "rxjs"; import type { ConcordiumSigner, ConcordiumNetwork } from "@ledgerhq/coin-concordium/types"; import { serializeTransaction, serializeCredentialDeploymentValues, serializeIdOwnershipProofs, encodeWord64, type Address, type CredentialDeploymentTransaction, type AnyTransaction, type SigningResult, } from "@ledgerhq/concordium-core"; import { DeviceActionStatus, type DeviceActionState, type DeviceManagementKit, } from "@ledgerhq/device-management-kit"; import { ConcordiumAddressVerificationFailedError, ConcordiumAppOutdatedError, ConcordiumInvalidMaxFeeError, ConcordiumInvalidPltPayloadError, ConcordiumSignerProtocolError, ConcordiumTrustedMetadataServiceError, } from "@ledgerhq/coin-concordium/types"; import { LockedDeviceError } from "@ledgerhq/hw-transport/errors"; import { UserRefusedOnDevice } from "./errors"; import { type SignerConcordium, SignerConcordiumBuilder, type GetPublicKeyDAError, type SignTransactionDAError, type SignCredentialDeploymentTransactionDAError, type VerifyAddressDAError, } from "@ledgerhq/device-signer-kit-concordium"; type DAError = | GetPublicKeyDAError | SignTransactionDAError | SignCredentialDeploymentTransactionDAError | VerifyAddressDAError; export class DmkSignerConcordium implements ConcordiumSigner { private readonly signer: SignerConcordium; constructor(dmk: DeviceManagementKit, sessionId: string) { this.signer = new SignerConcordiumBuilder({ dmk, sessionId }).build(); } async getPublicKey(path: string, confirm = false): Promise { const { observable } = this.signer.getPublicKey(path, { checkOnDevice: confirm, skipOpenApp: true, }); const result = this.mapResult(await lastValueFrom(observable)); return Buffer.from(result.publicKey).toString("hex"); } async getAddress( path: string, display = false, _id?: number, _cred?: number, _idp?: number, ): Promise
{ if (display) { throw new Error( "getAddress(display=true) is not yet supported via DMK signer: on-device address verification is unavailable", ); } const publicKey = await this.getPublicKey(path, false); return { address: publicKey, publicKey }; } async signTransaction(tx: AnyTransaction, path: string, maxFee: bigint): Promise { const serialized = serializeTransaction(tx); const { observable } = this.signer.signTransaction(path, new Uint8Array(serialized), maxFee, { skipOpenApp: true, }); const result = this.mapResult(await lastValueFrom(observable)); const signature = Buffer.from(result).toString("hex"); return { signature, serialized: serialized.toString("hex") }; } async signCredentialDeployment( tx: CredentialDeploymentTransaction, path: string, ): Promise { const transaction = this.serializeCredentialDeploymentToBytes(tx); const { observable } = this.signer.signCredentialDeploymentTransaction(path, transaction, { skipOpenApp: true, }); const result = this.mapResult(await lastValueFrom(observable)); return Buffer.from(result).toString("hex"); } async verifyAddress(path: string, address: string, network: ConcordiumNetwork): Promise { const { observable } = this.signer.verifyAddress(path, address, network, { skipOpenApp: true, }); const confirmed = this.mapResult(await lastValueFrom(observable)); if (confirmed !== true) { throw new Error("Address verification did not complete on the device"); } } private serializeCredentialDeploymentToBytes(tx: CredentialDeploymentTransaction): Uint8Array { const credentialValues = serializeCredentialDeploymentValues(tx); const proofs = serializeIdOwnershipProofs(tx.proofs); const proofLength = Buffer.alloc(4); proofLength.writeUInt32BE(proofs.length, 0); const expiry = encodeWord64(tx.expiry); const newOrExisting = Buffer.from([0x00]); return new Uint8Array( Buffer.concat([credentialValues, proofLength, proofs, newOrExisting, expiry]), ); } private mapResult(actionState: DeviceActionState): T { switch (actionState.status) { case DeviceActionStatus.Completed: return actionState.output; case DeviceActionStatus.Error: throw this.mapError(actionState.error); default: throw new Error("Unexpected device action status"); } } private mapError(error: E): Error { const originalMessage = this.originalErrorMessage(error); if (!("errorCode" in error)) { return new Error(this.formatGenericMessage(error._tag, originalMessage)); } switch (error.errorCode) { case "5515": return new LockedDeviceError(); case "6985": return new UserRefusedOnDevice(); case "trusted_metadata_service_error": return new ConcordiumTrustedMetadataServiceError(originalMessage); case "address_verification_failed": return new ConcordiumAddressVerificationFailedError(originalMessage); case "invalid_max_fee": return new ConcordiumInvalidMaxFeeError(originalMessage); case "6b04": case "6b0d": case "6b0e": case "6b0f": case "6b10": case "6b11": case "invalid_plt_transaction": return new ConcordiumInvalidPltPayloadError(originalMessage); case "6b00": case "6b01": case "6b02": case "6b03": case "6b06": case "6b07": case "unsupported_transaction_type": return new ConcordiumSignerProtocolError(originalMessage); // 6d00 means the app does not know the instruction at all, which is how a // pre-PLT app answers INS 0x27. It is treated as an outdated app rather // than a protocol defect, so the version guard still works if the shipped // app version differs from the one the signer pins. case "6d00": case "unsupported_app_version": return new ConcordiumAppOutdatedError(originalMessage); default: return new Error(this.formatGenericMessage(error._tag, originalMessage)); } } private originalErrorMessage(error: E): string | undefined { return "originalError" in error && error.originalError instanceof Error ? error.originalError.message : undefined; } private formatGenericMessage(tag: string, originalMessage: string | undefined): string { return originalMessage ? `${tag}: ${originalMessage}` : tag; } }