@ledgerhq/hw-app-near
Version:
Ledger Hardware Wallet Near Application API
141 lines (126 loc) • 4.13 kB
text/typescript
/********************************************************************************
* Ledger Node JS API
* (c) 2017-2018 Ledger
*
* 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.
********************************************************************************/
import type Transport from "@ledgerhq/hw-transport";
import { PublicKey } from "near-api-js/lib/utils";
import { KeyType } from "near-api-js/lib/utils/key_pair";
import { bip32PathToBytes } from "./utils";
// Based on https://github.com/LedgerHQ/ledger-secure-sdk/blob/master/include/os_io.h#L16
// 255B + CLA + INS + P1 + P2 + Lc
const CHUNK_SIZE = 255;
const CLA = 0x80;
const INS_GET_PUBLIC_KEY = 4;
const INS_GET_ADDRESS = 5;
const INS_SIGN = 2;
const P1_LAST_CHUNK = 0x80;
const NETWORK_ID = "W".charCodeAt(0);
/**
* NEAR API
*
* @example
* import Near from "@ledgerhq/hw-app-near";
* const near = new Near(transport)
*/
export default class Near {
transport: Transport;
constructor(transport: Transport) {
this.transport = transport;
transport.decorateAppAPIMethods(this, ["getPublicKey", "getAddress", "sign"], "NEAR");
}
/**
* @param path
* @option verify - if true, user must verify if the address is correct on the device
* @return an object with a publicKey and address
* @example
* near.getAddress("44'/397'/0'/0'/0'", true).then(o => o.address)
*/
async getAddress(
path: string,
verify?: boolean,
): Promise<{
publicKey: string;
address: string;
}> {
const client = await createClient(this.transport);
if (verify) {
await client.getAddress(path);
}
const rawPublicKey = await client.getPublicKey(path, false);
const publicKey = new PublicKey({
keyType: KeyType.ED25519,
data: rawPublicKey,
});
return {
address: rawPublicKey.toString("hex"),
publicKey: publicKey.toString(),
};
}
/**
* @param transaction
* @param path
* @return a signature to be broadcasted to the chain
*/
async signTransaction(transaction: Uint8Array, path: string): Promise<Buffer | undefined> {
const client = await createClient(this.transport);
const signature = await client.sign(transaction, path);
return signature;
}
}
async function createClient(transport) {
return {
transport,
async getPublicKey(path, verify) {
const response = await this.transport.send(
CLA,
INS_GET_PUBLIC_KEY,
verify ? 0 : 1,
NETWORK_ID,
bip32PathToBytes(path),
);
return Buffer.from(response.subarray(0, -2));
},
async getAddress(path) {
const response = await this.transport.send(
CLA,
INS_GET_ADDRESS,
0,
NETWORK_ID,
bip32PathToBytes(path),
);
return Buffer.from(response.subarray(0, -2));
},
async sign(transactionData, path) {
transactionData = Buffer.from(transactionData);
const concatenatedData = Buffer.concat([bip32PathToBytes(path), transactionData]);
const chunks = new Array(Math.ceil(concatenatedData.length / CHUNK_SIZE))
.fill("")
.map((_, i) => concatenatedData.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE));
for (const chunk of chunks) {
const isLastChunk = chunks.indexOf(chunk) === chunks.length - 1;
const response = await this.transport.send(
CLA,
INS_SIGN,
isLastChunk ? P1_LAST_CHUNK : 0,
NETWORK_ID,
chunk,
);
if (isLastChunk) {
return Buffer.from(response.subarray(0, -2));
}
}
},
};
}