@zondax/ledger-substrate
Version:
TS / Node API for Substrate/Polkadot based apps running on Ledger devices
440 lines • 22.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PolkadotGenericApp = void 0;
/** ******************************************************************************
* (c) 2019 - 2024 Zondax AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************* */
const axios_1 = __importDefault(require("axios"));
const ledger_js_1 = __importStar(require("@zondax/ledger-js"));
const common_1 = require("./common");
class PolkadotGenericApp extends ledger_js_1.default {
/**
* Constructs a new PolkadotGenericApp instance.
* @param transport - The transport instance.
* @param txMetadataChainId - The chain ID in the transaction metadata service.
* @param txMetadataSrvUrl - The optional transaction metadata service URL.
* @throws {Error} - If the transport is not defined.
*/
constructor(transport, txMetadataChainId, txMetadataSrvUrl) {
super(transport, PolkadotGenericApp._params);
this.txMetadataSrvUrl = txMetadataSrvUrl;
this.txMetadataChainId = txMetadataChainId;
if (!this.transport) {
throw new Error('Transport has not been defined');
}
}
/**
* Retrieves transaction metadata from the metadata service.
* @param txBlob - The transaction blob.
* @param txMetadataChainId - The optional chain ID for the transaction metadata service. This value temporarily overrides the one set in the constructor.
* @param txMetadataSrvUrl - The optional URL for the transaction metadata service. This value temporarily overrides the one set in the constructor.
* @returns The transaction metadata.
* @throws {ResponseError} - If the txMetadataSrvUrl is not defined.
*/
async getTxMetadata(txBlob, txMetadataChainId, txMetadataSrvUrl) {
const txMetadataChainIdVal = txMetadataChainId ?? this.txMetadataChainId;
const txMetadataSrvUrlVal = txMetadataSrvUrl ?? this.txMetadataSrvUrl;
if (!txMetadataChainIdVal) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.GenericError, 'txMetadataSrvUrl is not defined or is empty. The use of the method requires access to a metadata shortening service.');
}
if (!txMetadataSrvUrlVal) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.GenericError, 'txMetadataChainId is not defined or is empty. These values are configured in the metadata shortening service. Check the corresponding configuration in the service.');
}
const resp = await axios_1.default.post(txMetadataSrvUrlVal, {
txBlob: txBlob.toString('hex'),
chain: { id: txMetadataChainIdVal },
});
let txMetadata = resp.data.txMetadata;
if (txMetadata.slice(0, 2) === '0x') {
txMetadata = txMetadata.slice(2);
}
return Buffer.from(txMetadata, 'hex');
}
/**
* @deprecated Use getAddressEcdsa or getAddressEd25519 instead. This method will be removed in a future version.
* Retrieves the address for a given BIP44 path and SS58 prefix.
* @param bip44Path - The BIP44 path.
* @param ss58prefix - The SS58 prefix, must be an integer up to 65535.
* @param showAddrInDevice - Whether to show the address on the device.
* @param scheme - The scheme to use for the address. Default is ED25519.
* @returns The address response.
* @throws {ResponseError} If the response from the device indicates an error.
*/
async getAddress(bip44Path, ss58prefix, showAddrInDevice = false, scheme = 0 /* SCHEME.ED25519 */) {
// needs to be integer, and up to 65535
if (!Number.isInteger(ss58prefix) || ss58prefix < 0 || ss58prefix >> 16 !== 0) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.ConditionsOfUseNotSatisfied, `Unexpected ss58prefix ${ss58prefix}. Needs to be a non-negative integer up to 2^16`);
}
if (scheme != 2 /* SCHEME.ECDSA */ && scheme != 0 /* SCHEME.ED25519 */) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.ConditionsOfUseNotSatisfied, `Unexpected scheme ${scheme}. Needs to be ECDSA (2) or ED25519 (0)`);
}
const bip44PathBuffer = this.serializePath(bip44Path);
const prefixBuffer = Buffer.alloc(2);
prefixBuffer.writeUInt16LE(ss58prefix);
let payload = bip44PathBuffer;
if (scheme === 0 /* SCHEME.ED25519 */) {
payload = Buffer.concat([payload, prefixBuffer]);
}
const p1 = showAddrInDevice ? 1 /* P1_VALUES.SHOW_ADDRESS_IN_DEVICE */ : 0 /* P1_VALUES.ONLY_RETRIEVE */;
try {
const responseBuffer = await this.transport.send(this.CLA, this.INS.GET_ADDR, p1, scheme ?? 0 /* SCHEME.ED25519 */, payload);
const response = (0, ledger_js_1.processResponse)(responseBuffer);
const currentScheme = (scheme ?? 0 /* SCHEME.ED25519 */);
const pubKeyLen = currentScheme === 2 /* SCHEME.ECDSA */ ? common_1.ECDSA_PUBKEY_LEN : common_1.ED25519_PUBKEY_LEN;
const pubKey = response.readBytes(pubKeyLen).toString('hex');
let address = '';
if (currentScheme === 2 /* SCHEME.ECDSA */) {
address = response.readBytes(response.length()).toString('hex');
}
else {
address = response.readBytes(response.length()).toString('ascii');
}
return {
pubKey,
address,
};
}
catch (e) {
throw (0, ledger_js_1.processErrorResponse)(e);
}
}
/**
* Retrieves the address for a given BIP44 path using the ECDSA scheme.
* @param bip44Path - The BIP44 path.
* @param showAddrInDevice - Whether to show the address on the device.
* @returns The address response.
* @throws {ResponseError} If the response from the device indicates an error.
*/
async getAddressEcdsa(bip44Path, showAddrInDevice = false) {
return this.getAddress(bip44Path, 0, showAddrInDevice, 2 /* SCHEME.ECDSA */);
}
/**
* Retrieves the address for a given BIP44 path and SS58 prefix using the ED25519 scheme.
* @param bip44Path - The BIP44 path.
* @param ss58prefix - The SS58 prefix.
* @param showAddrInDevice - Whether to show the address on the device.
* @returns The address response.
* @throws {ResponseError} If the response from the device indicates an error.
*/
async getAddressEd25519(bip44Path, ss58prefix, showAddrInDevice = false) {
return this.getAddress(bip44Path, ss58prefix, showAddrInDevice);
}
splitBufferToChunks(message, chunkSize) {
const chunks = [];
const buffer = Buffer.from(message);
for (let i = 0; i < buffer.length; i += chunkSize) {
let end = i + chunkSize;
if (i > buffer.length) {
end = buffer.length;
}
chunks.push(buffer.subarray(i, end));
}
return chunks;
}
getSignReqChunks(path, txBlob, metadata) {
const chunks = [];
const bip44Path = this.serializePath(path);
const blobLen = Buffer.alloc(2);
blobLen.writeUInt16LE(txBlob.length);
chunks.push(Buffer.concat([bip44Path, blobLen]));
if (metadata == null) {
chunks.push(...this.splitBufferToChunks(txBlob, this.CHUNK_SIZE));
}
else {
chunks.push(...this.splitBufferToChunks(Buffer.concat([txBlob, metadata]), this.CHUNK_SIZE));
}
return chunks;
}
/**
* Signs a transaction blob.
* @param path - The BIP44 path.
* @param ins - The instruction for signing.
* @param blob - The transaction blob.
* @param metadata - The optional metadata.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signImplEd25519(path, ins, blob, metadata) {
const chunks = this.getSignReqChunks(path, blob, metadata);
try {
let result = await this.sendGenericChunk(ins, 0 /* SCHEME.ED25519 */, 1, chunks.length, chunks[0]);
for (let i = 1; i < chunks.length; i += 1) {
result = await this.sendGenericChunk(ins, 0 /* SCHEME.ED25519 */, 1 + i, chunks.length, chunks[i]);
}
return {
signature: result.readBytes(result.length()),
};
}
catch (e) {
throw (0, ledger_js_1.processErrorResponse)(e);
}
}
/**
* Signs a transaction blob using the ECDSA scheme.
* @param path - The BIP44 path.
* @param ins - The instruction for signing.
* @param blob - The transaction blob.
* @param metadata - The optional metadata.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status. For ECDSA, the signature is in RSV format:
* - R: First 32 bytes (signature.slice(0, 32))
* - S: Next 32 bytes (signature.slice(32, 64))
* - V: Last byte (signature.slice(64, 65))
* @see parseEcdsaSignature - Use this utility function to easily parse the signature into R, S, V components
*/
async signImplEcdsa(path, ins, blob, metadata) {
const chunks = this.getSignReqChunks(path, blob, metadata);
try {
let result = await this.sendGenericChunk(ins, 2 /* SCHEME.ECDSA */, 1, chunks.length, chunks[0]);
for (let i = 1; i < chunks.length; i += 1) {
result = await this.sendGenericChunk(ins, 2 /* SCHEME.ECDSA */, 1 + i, chunks.length, chunks[i]);
}
return {
signature: result.readBytes(result.length()),
};
}
catch (e) {
throw (0, ledger_js_1.processErrorResponse)(e);
}
}
/**
* @deprecated Use signEcdsa or signEd25519 instead. This method will be removed in a future version.
* Signs a transaction blob retrieving the correct metadata from a metadata service.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param scheme - The scheme to use for the signing. Default is ED25519.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async sign(path, txBlob, scheme = 0 /* SCHEME.ED25519 */) {
if (scheme != 2 /* SCHEME.ECDSA */ && scheme != 0 /* SCHEME.ED25519 */) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.ConditionsOfUseNotSatisfied, `Unexpected scheme ${scheme}. Needs to be ECDSA (2) or ED25519 (0)`);
}
if (scheme === 2 /* SCHEME.ECDSA */) {
return await this.signEcdsa(path, txBlob);
}
return await this.signEd25519(path, txBlob);
}
/**
* Signs a transaction blob using the ED25519 scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signEd25519(path, txBlob) {
if (!this.txMetadataSrvUrl) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.GenericError, 'txMetadataSrvUrl is not defined or is empty. The use of the method requires access to a metadata shortening service.');
}
if (!this.txMetadataChainId) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.GenericError, 'txMetadataChainId is not defined or is empty. These values are configured in the metadata shortening service. Check the corresponding configuration in the service.');
}
const txMetadata = await this.getTxMetadata(txBlob);
return await this.signImplEd25519(path, this.INS.SIGN, txBlob, txMetadata);
}
/**
* Signs a transaction blob using the ECDSA scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status. For ECDSA, the signature is in RSV format:
* - R: First 32 bytes (signature.slice(0, 32))
* - S: Next 32 bytes (signature.slice(32, 64))
* - V: Last byte (signature.slice(64, 65))
* @see parseEcdsaSignature - Use this utility function to easily parse the signature into R, S, V components
*/
async signEcdsa(path, txBlob) {
if (!this.txMetadataSrvUrl) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.GenericError, 'txMetadataSrvUrl is not defined or is empty. The use of the method requires access to a metadata shortening service.');
}
if (!this.txMetadataChainId) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.GenericError, 'txMetadataChainId is not defined or is empty. These values are configured in the metadata shortening service. Check the corresponding configuration in the service.');
}
const txMetadata = await this.getTxMetadata(txBlob);
return await this.signImplEcdsa(path, this.INS.SIGN, txBlob, txMetadata);
}
/**
* Signs a transaction blob with provided metadata.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txMetadataChainId - The optional chain ID for the transaction metadata service. This value temporarily overrides the one set in the constructor.
* @param txMetadataSrvUrl - The optional URL for the transaction metadata service. This value temporarily overrides the one set in the constructor.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signMigration(path, txBlob, txMetadataChainId, txMetadataSrvUrl) {
if (!this.txMetadataSrvUrl) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.GenericError, 'txMetadataSrvUrl is not defined or is empty. The use of the method requires access to a metadata shortening service.');
}
if (!this.txMetadataChainId) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.GenericError, 'txMetadataChainId is not defined or is empty. These values are configured in the metadata shortening service. Check the corresponding configuration in the service.');
}
const txMetadata = await this.getTxMetadata(txBlob, txMetadataChainId, txMetadataSrvUrl);
return await this.signImplEd25519(path, this.INS.SIGN, txBlob, txMetadata);
}
/**
* @deprecated Use signRawEcdsa or signRawEd25519 instead. This method will be removed in a future version.
* Signs a raw transaction blob.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param scheme - The scheme to use for the signing. Default is ED25519.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signRaw(path, txBlob, scheme = 0 /* SCHEME.ED25519 */) {
if (scheme != 2 /* SCHEME.ECDSA */ && scheme != 0 /* SCHEME.ED25519 */) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.ConditionsOfUseNotSatisfied, `Unexpected scheme ${scheme}. Needs to be ECDSA (2) or ED25519 (0)`);
}
if (scheme === 2 /* SCHEME.ECDSA */) {
return await this.signRawEcdsa(path, txBlob);
}
return await this.signRawEd25519(path, txBlob);
}
/**
* Signs a raw transaction blob using the ED25519 scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signRawEd25519(path, txBlob) {
return await this.signImplEd25519(path, this.INS.SIGN_RAW, txBlob);
}
/**
* Signs a raw transaction blob using the ECDSA scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status. For ECDSA, the signature is in RSV format:
* - R: First 32 bytes (signature.slice(0, 32))
* - S: Next 32 bytes (signature.slice(32, 64))
* - V: Last byte (signature.slice(64, 65))
* @see parseEcdsaSignature - Use this utility function to easily parse the signature into R, S, V components
*/
async signRawEcdsa(path, txBlob) {
return await this.signImplEcdsa(path, this.INS.SIGN_RAW, txBlob);
}
/**
* @deprecated Use signWithMetadataEd25519 or signWithMetadataEcdsa instead. This method will be removed in a future version.
* [Expert-only Method] Signs a transaction blob with provided metadata (this could be used also with a migration app)
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txMetadata - The transaction metadata.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signWithMetadata(path, txBlob, txMetadata, scheme = 0 /* SCHEME.ED25519 */) {
if (scheme != 2 /* SCHEME.ECDSA */ && scheme != 0 /* SCHEME.ED25519 */) {
throw new ledger_js_1.ResponseError(ledger_js_1.LedgerError.ConditionsOfUseNotSatisfied, `Unexpected scheme ${scheme}. Needs to be ECDSA (2) or ED25519 (0)`);
}
if (scheme === 2 /* SCHEME.ECDSA */) {
return await this.signWithMetadataEcdsa(path, txBlob, txMetadata);
}
return await this.signWithMetadataEd25519(path, txBlob, txMetadata);
}
/**
* Signs a transaction blob with provided metadata using the ECDSA scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txMetadata - The transaction metadata.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status. For ECDSA, the signature is in RSV format:
* - R: First 32 bytes (signature.slice(0, 32))
* - S: Next 32 bytes (signature.slice(32, 64))
* - V: Last byte (signature.slice(64, 65))
* @see parseEcdsaSignature - Use this utility function to easily parse the signature into R, S, V components
*/
async signWithMetadataEcdsa(path, txBlob, txMetadata) {
return await this.signImplEcdsa(path, this.INS.SIGN, txBlob, txMetadata);
}
/**
* Signs a transaction blob with provided metadata using the ED25519 scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txMetadata - The transaction metadata.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signWithMetadataEd25519(path, txBlob, txMetadata) {
return await this.signImplEd25519(path, this.INS.SIGN, txBlob, txMetadata);
}
/**
* Utility function to convert ECDSA signature response into RSV structure
* @param signature - The ECDSA signature buffer from the device response
* @returns Object containing R, S, and V components of the ECDSA signature
* @throws {Error} If signature length is not 65 bytes (expected for ECDSA RSV format)
*/
static parseEcdsaSignature(signature) {
if (signature.length !== 65) {
throw new Error('Invalid ECDSA signature length. Expected 65 bytes for RSV format');
}
return {
r: signature.slice(0, 32).toString('hex'),
s: signature.slice(32, 64).toString('hex'),
v: signature.slice(64, 65).toString('hex'),
};
}
}
exports.PolkadotGenericApp = PolkadotGenericApp;
PolkadotGenericApp._INS = {
GET_VERSION: 0x00,
GET_ADDR: 0x01,
SIGN: 0x02,
SIGN_RAW: 0x03,
};
PolkadotGenericApp._params = {
cla: 0xf9,
ins: { ...PolkadotGenericApp._INS },
p1Values: { ONLY_RETRIEVE: 0x00, SHOW_ADDRESS_IN_DEVICE: 0x01 },
chunkSize: 250,
requiredPathLengths: [5],
};
//# sourceMappingURL=generic_app.js.map