@blooo/hw-app-concordium
Version:
Ledger Hardware Wallet Concordium Application API
1,129 lines (990 loc) • 32.4 kB
text/typescript
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;
}
}
}