@near-wallet-selector/ledger
Advanced tools
Comparing version 3.1.0 to 4.0.0-alpha.0
{ | ||
"name": "@near-wallet-selector/ledger", | ||
"version": "3.1.0", | ||
"version": "4.0.0-alpha.0", | ||
"main": "./src/index.js", | ||
@@ -11,9 +11,8 @@ "typings": "./src/index.d.ts", | ||
"bn.js": "^5.2.0", | ||
"@ledgerhq/logs": "^6.10.0", | ||
"@near-wallet-selector/core": "3.1.0", | ||
"react": "^17.0.2", | ||
"react-dom": "^17.0.2", | ||
"is-mobile": "^3.0.0" | ||
"@near-wallet-selector/core": "4.0.0-alpha.0", | ||
"rxjs": "~7.4.0", | ||
"is-mobile": "^3.0.0", | ||
"@near-wallet-selector/wallet-utils": "4.0.0-alpha.0" | ||
}, | ||
"peerDependencies": {} | ||
} |
@@ -20,3 +20,3 @@ # @near-wallet-selector/ledger | ||
```ts | ||
import NearWalletSelector from "@near-wallet-selector/core"; | ||
import { setupWalletSelector } from "@near-wallet-selector/core"; | ||
import { setupLedger } from "@near-wallet-selector/ledger"; | ||
@@ -29,6 +29,6 @@ | ||
const selector = await NearWalletSelector.init({ | ||
const selector = await setupWalletSelector({ | ||
network: "testnet", | ||
contractId: "guest-book.testnet", | ||
wallets: [ledger], | ||
modules: [ledger], | ||
}); | ||
@@ -35,0 +35,0 @@ ``` |
/// <reference types="node" /> | ||
import { Log } from "@ledgerhq/logs"; | ||
export declare const CLA = 128; | ||
@@ -26,10 +25,8 @@ export declare const INS_SIGN = 2; | ||
} | ||
export declare const isLedgerSupported: () => boolean; | ||
export declare class LedgerClient { | ||
private transport; | ||
static isSupported: () => boolean; | ||
isConnected: () => boolean; | ||
connect: () => Promise<void>; | ||
disconnect: () => Promise<void>; | ||
listen: (callback: (data: Log) => void) => { | ||
remove: () => void; | ||
}; | ||
setScrambleKey: (key: string) => void; | ||
@@ -36,0 +33,0 @@ on: <Event_1 extends "disconnect">(event: Event_1, callback: (data: EventMap[Event_1]) => void) => Subscription; |
import { __awaiter } from "tslib"; | ||
import TransportWebHID from "@ledgerhq/hw-transport-webhid"; | ||
import { listen } from "@ledgerhq/logs"; | ||
import { utils } from "near-api-js"; | ||
@@ -37,30 +36,54 @@ // Further reading regarding APDU Ledger API: | ||
export const networkId = "W".charCodeAt(0); | ||
// TODO: Needs a method to assert whether we're connected. | ||
// Not using TransportWebHID.isSupported as it's chosen to use a Promise... | ||
export const isLedgerSupported = () => { | ||
var _a; | ||
return !!((_a = window.navigator) === null || _a === void 0 ? void 0 : _a.hid); | ||
}; | ||
export class LedgerClient { | ||
constructor() { | ||
this.transport = null; | ||
this.isConnected = () => { | ||
return Boolean(this.transport); | ||
}; | ||
this.connect = () => __awaiter(this, void 0, void 0, function* () { | ||
this.transport = yield TransportWebHID.create(); | ||
const handleDisconnect = () => { | ||
var _a; | ||
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.off("disconnect", handleDisconnect); | ||
this.transport = null; | ||
}; | ||
this.transport.on("disconnect", handleDisconnect); | ||
}); | ||
this.disconnect = () => { | ||
return this.transport.close(); | ||
}; | ||
this.listen = (callback) => { | ||
const unsubscribe = listen(callback); | ||
return { | ||
remove: () => unsubscribe(), | ||
}; | ||
}; | ||
this.disconnect = () => __awaiter(this, void 0, void 0, function* () { | ||
if (!this.transport) { | ||
throw new Error("Device not connected"); | ||
} | ||
yield this.transport.close(); | ||
this.transport = null; | ||
}); | ||
this.setScrambleKey = (key) => { | ||
if (!this.transport) { | ||
throw new Error("Device not connected"); | ||
} | ||
this.transport.setScrambleKey(key); | ||
}; | ||
this.on = (event, callback) => { | ||
if (!this.transport) { | ||
throw new Error("Device not connected"); | ||
} | ||
this.transport.on(event, callback); | ||
return { | ||
remove: () => this.transport.off(event, callback), | ||
remove: () => { var _a; return (_a = this.transport) === null || _a === void 0 ? void 0 : _a.off(event, callback); }, | ||
}; | ||
}; | ||
this.off = (event, callback) => { | ||
if (!this.transport) { | ||
throw new Error("Device not connected"); | ||
} | ||
this.transport.off(event, callback); | ||
}; | ||
this.getVersion = () => __awaiter(this, void 0, void 0, function* () { | ||
if (!this.transport) { | ||
throw new Error("Device not connected"); | ||
} | ||
const res = yield this.transport.send(CLA, INS_GET_APP_VERSION, P1_IGNORE, P2_IGNORE); | ||
@@ -71,2 +94,5 @@ const [major, minor, patch] = Array.from(res); | ||
this.getPublicKey = ({ derivationPath }) => __awaiter(this, void 0, void 0, function* () { | ||
if (!this.transport) { | ||
throw new Error("Device not connected"); | ||
} | ||
const res = yield this.transport.send(CLA, INS_GET_PUBLIC_KEY, P2_IGNORE, networkId, parseDerivationPath(derivationPath)); | ||
@@ -76,2 +102,5 @@ return utils.serialize.base_encode(res.subarray(0, -2)); | ||
this.sign = ({ data, derivationPath }) => __awaiter(this, void 0, void 0, function* () { | ||
if (!this.transport) { | ||
throw new Error("Device not connected"); | ||
} | ||
// NOTE: getVersion call resets state to avoid starting from partially filled buffer | ||
@@ -96,7 +125,2 @@ yield this.getVersion(); | ||
} | ||
// Not using TransportWebHID.isSupported as it's chosen to use a Promise... | ||
LedgerClient.isSupported = () => { | ||
var _a; | ||
return !!((_a = window.navigator) === null || _a === void 0 ? void 0 : _a.hid); | ||
}; | ||
//# sourceMappingURL=ledger-client.js.map |
@@ -1,6 +0,6 @@ | ||
import { HardwareWallet, WalletModule } from "@near-wallet-selector/core"; | ||
import type { WalletModuleFactory, HardwareWallet } from "@near-wallet-selector/core"; | ||
export interface LedgerParams { | ||
iconUrl?: string; | ||
} | ||
export declare const LOCAL_STORAGE_AUTH_DATA = "ledger:authData"; | ||
export declare function setupLedger({ iconUrl, }?: LedgerParams): WalletModule<HardwareWallet>; | ||
export declare const STORAGE_ACCOUNTS = "accounts"; | ||
export declare function setupLedger({ iconUrl, }?: LedgerParams): WalletModuleFactory<HardwareWallet>; |
import { __awaiter } from "tslib"; | ||
import { transactions as nearTransactions, utils } from "near-api-js"; | ||
import { isMobile } from "is-mobile"; | ||
import { TypedError } from "near-api-js/lib/utils/errors"; | ||
import isMobile from "is-mobile"; | ||
import { transformActions, } from "@near-wallet-selector/core"; | ||
import { LedgerClient } from "./ledger-client"; | ||
export const LOCAL_STORAGE_AUTH_DATA = `ledger:authData`; | ||
export function setupLedger({ iconUrl, } = {}) { | ||
return function Ledger({ provider, emitter, logger, storage, updateState }) { | ||
let client; | ||
const subscriptions = {}; | ||
const state = { authData: null }; | ||
const debugMode = false; | ||
const getAccounts = () => { | ||
var _a; | ||
const accountId = (_a = state.authData) === null || _a === void 0 ? void 0 : _a.accountId; | ||
if (!accountId) { | ||
return []; | ||
import { signTransactions } from "@near-wallet-selector/wallet-utils"; | ||
import { isLedgerSupported, LedgerClient } from "./ledger-client"; | ||
import { utils } from "near-api-js"; | ||
export const STORAGE_ACCOUNTS = "accounts"; | ||
const setupLedgerState = (storage) => __awaiter(void 0, void 0, void 0, function* () { | ||
const accounts = yield storage.getItem(STORAGE_ACCOUNTS); | ||
return { | ||
client: new LedgerClient(), | ||
subscriptions: [], | ||
accounts: accounts || [], | ||
}; | ||
}); | ||
const Ledger = ({ options, store, provider, logger, storage, }) => __awaiter(void 0, void 0, void 0, function* () { | ||
const _state = yield setupLedgerState(storage); | ||
const signer = { | ||
createKey: () => { | ||
throw new Error("Not implemented"); | ||
}, | ||
getPublicKey: (accountId) => __awaiter(void 0, void 0, void 0, function* () { | ||
const account = _state.accounts.find((a) => a.accountId === accountId); | ||
if (!account) { | ||
throw new Error("Failed to find public key for account"); | ||
} | ||
return [{ accountId }]; | ||
}; | ||
const signOut = () => __awaiter(this, void 0, void 0, function* () { | ||
for (const key in subscriptions) { | ||
subscriptions[key].remove(); | ||
return utils.PublicKey.from(account.publicKey); | ||
}), | ||
signMessage: (message, accountId) => __awaiter(void 0, void 0, void 0, function* () { | ||
const account = _state.accounts.find((a) => a.accountId === accountId); | ||
if (!account) { | ||
throw new Error("Failed to find account for signing"); | ||
} | ||
storage.removeItem(LOCAL_STORAGE_AUTH_DATA); | ||
// Only close if we've already connected. | ||
if (client) { | ||
yield client.disconnect(); | ||
} | ||
updateState((prevState) => (Object.assign(Object.assign({}, prevState), { selectedWalletId: null }))); | ||
state.authData = null; | ||
client = null; | ||
const accounts = getAccounts(); | ||
emitter.emit("accountsChanged", { accounts }); | ||
emitter.emit("signOut", { accounts }); | ||
}); | ||
const getClient = () => __awaiter(this, void 0, void 0, function* () { | ||
if (client) { | ||
return client; | ||
} | ||
const ledgerClient = new LedgerClient(); | ||
yield ledgerClient.connect(); | ||
ledgerClient.setScrambleKey("NEAR"); | ||
subscriptions["disconnect"] = ledgerClient.on("disconnect", (err) => { | ||
const signature = yield _state.client.sign({ | ||
data: message, | ||
derivationPath: account.derivationPath, | ||
}); | ||
return { | ||
signature, | ||
publicKey: utils.PublicKey.from(account.publicKey), | ||
}; | ||
}), | ||
}; | ||
const getAccounts = () => { | ||
return _state.accounts.map((x) => ({ | ||
accountId: x.accountId, | ||
})); | ||
}; | ||
const cleanup = () => { | ||
_state.subscriptions.forEach((subscription) => subscription.remove()); | ||
_state.subscriptions = []; | ||
_state.accounts = []; | ||
storage.removeItem(STORAGE_ACCOUNTS); | ||
}; | ||
const signOut = () => __awaiter(void 0, void 0, void 0, function* () { | ||
if (_state.client.isConnected()) { | ||
yield _state.client.disconnect().catch((err) => { | ||
logger.log("Failed to disconnect device"); | ||
logger.error(err); | ||
signOut(); | ||
}); | ||
if (debugMode) { | ||
subscriptions["logs"] = ledgerClient.listen((data) => { | ||
logger.log("Ledger:init:logs", data); | ||
}); | ||
} | ||
cleanup(); | ||
}); | ||
const connectLedgerDevice = () => __awaiter(void 0, void 0, void 0, function* () { | ||
if (_state.client.isConnected()) { | ||
return; | ||
} | ||
yield _state.client.connect(); | ||
}); | ||
const validateAccessKey = ({ accountId, publicKey, }) => { | ||
logger.log("validateAccessKey", { accountId, publicKey }); | ||
return provider.viewAccessKey({ accountId, publicKey }).then((accessKey) => { | ||
logger.log("validateAccessKey:accessKey", { accessKey }); | ||
if (accessKey.permission !== "FullAccess") { | ||
throw new Error("Public key requires 'FullAccess' permission"); | ||
} | ||
client = ledgerClient; | ||
return ledgerClient; | ||
}); | ||
const validate = ({ accountId, derivationPath }) => __awaiter(this, void 0, void 0, function* () { | ||
logger.log("Ledger:validate", { accountId, derivationPath }); | ||
const ledgerClient = yield getClient(); | ||
const publicKey = yield ledgerClient.getPublicKey({ | ||
derivationPath: derivationPath, | ||
}); | ||
logger.log("Ledger:validate:publicKey", { publicKey }); | ||
try { | ||
const accessKey = yield provider.viewAccessKey({ | ||
accountId, | ||
publicKey, | ||
}); | ||
logger.log("Ledger:validate:accessKey", { accessKey }); | ||
if (accessKey.permission !== "FullAccess") { | ||
throw new Error("Public key requires 'FullAccess' permission"); | ||
} | ||
return { | ||
publicKey, | ||
accessKey, | ||
}; | ||
return accessKey; | ||
}, (err) => { | ||
if (err instanceof TypedError && err.type === "AccessKeyDoesNotExist") { | ||
return null; | ||
} | ||
catch (err) { | ||
if (err instanceof TypedError && err.type === "AccessKeyDoesNotExist") { | ||
return { | ||
publicKey, | ||
accessKey: null, | ||
}; | ||
} | ||
throw err; | ||
} | ||
throw err; | ||
}); | ||
const signTransaction = (transaction, ledgerClient, derivationPath) => __awaiter(this, void 0, void 0, function* () { | ||
const serializedTx = utils.serialize.serialize(nearTransactions.SCHEMA, transaction); | ||
const signature = yield ledgerClient.sign({ | ||
data: serializedTx, | ||
derivationPath, | ||
}); | ||
return new nearTransactions.SignedTransaction({ | ||
transaction, | ||
signature: new nearTransactions.Signature({ | ||
keyType: transaction.publicKey.keyType, | ||
data: signature, | ||
}), | ||
}); | ||
}; | ||
const getAccountIdFromPublicKey = ({ publicKey, }) => __awaiter(void 0, void 0, void 0, function* () { | ||
const response = yield fetch(`${options.network.helperUrl}/publicKey/ed25519:${publicKey}/accounts`); | ||
if (!response.ok) { | ||
throw new Error("Failed to get account id from public key"); | ||
} | ||
const accountIds = yield response.json(); | ||
if (!Array.isArray(accountIds) || !accountIds.length) { | ||
throw new Error("Failed to find account linked for public key: " + publicKey); | ||
} | ||
return accountIds[0]; | ||
}); | ||
const transformTransactions = (transactions) => { | ||
const accounts = getAccounts(); | ||
const { contract } = store.getState(); | ||
if (!accounts.length || !contract) { | ||
throw new Error("Wallet not signed in"); | ||
} | ||
return transactions.map((transaction) => { | ||
return { | ||
signerId: transaction.signerId || accounts[0].accountId, | ||
receiverId: transaction.receiverId || contract.contractId, | ||
actions: transaction.actions, | ||
}; | ||
}); | ||
const signTransactions = (transactions) => __awaiter(this, void 0, void 0, function* () { | ||
if (!state.authData) { | ||
throw new Error("Not signed in"); | ||
} | ||
const { accountId, derivationPath, publicKey } = state.authData; | ||
const ledgerClient = yield getClient(); | ||
const [block, accessKey] = yield Promise.all([ | ||
provider.block({ finality: "final" }), | ||
provider.viewAccessKey({ accountId, publicKey }), | ||
]); | ||
const signedTransactions = []; | ||
for (let i = 0; i < transactions.length; i++) { | ||
const actions = transformActions(transactions[i].actions); | ||
const transaction = nearTransactions.createTransaction(accountId, utils.PublicKey.from(publicKey), transactions[i].receiverId, accessKey.nonce + i + 1, actions, utils.serialize.base_decode(block.header.hash)); | ||
const signedTx = yield signTransaction(transaction, ledgerClient, derivationPath); | ||
signedTransactions.push(signedTx); | ||
} | ||
return signedTransactions; | ||
}); | ||
return { | ||
id: "ledger", | ||
type: "hardware", | ||
name: "Ledger", | ||
description: null, | ||
iconUrl: iconUrl || "./assets/ledger-icon.png", | ||
isAvailable() { | ||
if (!LedgerClient.isSupported()) { | ||
return false; | ||
}; | ||
return { | ||
signIn({ derivationPaths }) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const existingAccounts = getAccounts(); | ||
if (existingAccounts.length) { | ||
return existingAccounts; | ||
} | ||
if (isMobile()) { | ||
return false; | ||
if (!derivationPaths.length) { | ||
throw new Error("Invalid derivation paths"); | ||
} | ||
return true; | ||
}, | ||
init() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
state.authData = storage.getItem(LOCAL_STORAGE_AUTH_DATA); | ||
}); | ||
}, | ||
signIn({ accountId, derivationPath }) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (yield this.isSignedIn()) { | ||
return; | ||
// Note: Connection must be triggered by user interaction. | ||
yield connectLedgerDevice(); | ||
const accounts = []; | ||
for (let i = 0; i < derivationPaths.length; i += 1) { | ||
const derivationPath = derivationPaths[i]; | ||
const publicKey = yield _state.client.getPublicKey({ derivationPath }); | ||
const accountId = yield getAccountIdFromPublicKey({ publicKey }); | ||
if (accounts.some((x) => x.accountId === accountId)) { | ||
throw new Error("Duplicate account id: " + accountId); | ||
} | ||
if (!accountId) { | ||
throw new Error("Invalid account id"); | ||
} | ||
if (!derivationPath) { | ||
throw new Error("Invalid derivation path"); | ||
} | ||
const { publicKey, accessKey } = yield validate({ | ||
accountId, | ||
derivationPath, | ||
}); | ||
const accessKey = yield validateAccessKey({ accountId, publicKey }); | ||
if (!accessKey) { | ||
throw new Error(`Public key is not registered with the account '${accountId}'.`); | ||
} | ||
const authData = { | ||
accounts.push({ | ||
accountId, | ||
derivationPath, | ||
publicKey, | ||
}; | ||
storage.setItem(LOCAL_STORAGE_AUTH_DATA, authData); | ||
state.authData = authData; | ||
updateState((prevState) => (Object.assign(Object.assign({}, prevState), { showModal: false, selectedWalletId: this.id }))); | ||
const accounts = getAccounts(); | ||
emitter.emit("signIn", { accounts }); | ||
emitter.emit("accountsChanged", { accounts }); | ||
}); | ||
}, | ||
signOut, | ||
isSignedIn() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return !!state.authData; | ||
}); | ||
}, | ||
getAccounts() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return getAccounts(); | ||
}); | ||
}, | ||
signAndSendTransaction({ signerId, receiverId, actions }) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
logger.log("Ledger:signAndSendTransaction", { | ||
signerId, | ||
receiverId, | ||
actions, | ||
}); | ||
if (!state.authData) { | ||
throw new Error("Not signed in"); | ||
} | ||
const { accountId, derivationPath, publicKey } = state.authData; | ||
const ledgerClient = yield getClient(); | ||
const [block, accessKey] = yield Promise.all([ | ||
provider.block({ finality: "final" }), | ||
provider.viewAccessKey({ accountId, publicKey }), | ||
]); | ||
logger.log("Ledger:signAndSendTransaction:block", block); | ||
logger.log("Ledger:signAndSendTransaction:accessKey", accessKey); | ||
const transaction = nearTransactions.createTransaction(accountId, utils.PublicKey.from(publicKey), receiverId, accessKey.nonce + 1, transformActions(actions), utils.serialize.base_decode(block.header.hash)); | ||
const signedTx = yield signTransaction(transaction, ledgerClient, derivationPath); | ||
return provider.sendTransaction(signedTx); | ||
}); | ||
} | ||
yield storage.setItem(STORAGE_ACCOUNTS, accounts); | ||
_state.accounts = accounts; | ||
return getAccounts(); | ||
}); | ||
}, | ||
signOut, | ||
getAccounts() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return getAccounts(); | ||
}); | ||
}, | ||
signAndSendTransaction({ signerId, receiverId, actions }) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
logger.log("signAndSendTransaction", { signerId, receiverId, actions }); | ||
if (!_state.accounts.length) { | ||
throw new Error("Wallet not signed in"); | ||
} | ||
// Note: Connection must be triggered by user interaction. | ||
yield connectLedgerDevice(); | ||
const signedTransactions = yield signTransactions(transformTransactions([{ signerId, receiverId, actions }]), signer, options.network); | ||
return provider.sendTransaction(signedTransactions[0]); | ||
}); | ||
}, | ||
signAndSendTransactions({ transactions }) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
logger.log("signAndSendTransactions", { transactions }); | ||
if (!_state.accounts.length) { | ||
throw new Error("Wallet not signed in"); | ||
} | ||
// Note: Connection must be triggered by user interaction. | ||
yield connectLedgerDevice(); | ||
const signedTransactions = yield signTransactions(transformTransactions(transactions), signer, options.network); | ||
return Promise.all(signedTransactions.map((signedTx) => provider.sendTransaction(signedTx))); | ||
}); | ||
}, | ||
}; | ||
}); | ||
export function setupLedger({ iconUrl = "./assets/ledger-icon.png", } = {}) { | ||
return () => __awaiter(this, void 0, void 0, function* () { | ||
const mobile = isMobile(); | ||
const supported = isLedgerSupported(); | ||
if (mobile || !supported) { | ||
return null; | ||
} | ||
return { | ||
id: "ledger", | ||
type: "hardware", | ||
metadata: { | ||
name: "Ledger", | ||
description: null, | ||
iconUrl, | ||
deprecated: false, | ||
}, | ||
signAndSendTransactions({ transactions }) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const signedTransactions = yield signTransactions(transactions); | ||
return Promise.all(signedTransactions.map((signedTx) => provider.sendTransaction(signedTx))); | ||
}); | ||
}, | ||
init: Ledger, | ||
}; | ||
}; | ||
}); | ||
} | ||
//# sourceMappingURL=ledger.js.map |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
29863
8
364
2
2
+ Addedrxjs@~7.4.0
+ Added@near-wallet-selector/core@4.0.0-alpha.0(transitive)
+ Added@near-wallet-selector/wallet-utils@4.0.0-alpha.0(transitive)
+ Addedrxjs@7.4.0(transitive)
+ Addedtslib@2.1.0(transitive)
- Removed@ledgerhq/logs@^6.10.0
- Removedreact@^17.0.2
- Removedreact-dom@^17.0.2
- Removed@near-wallet-selector/core@3.1.0(transitive)
- Removedjs-tokens@4.0.0(transitive)
- Removedloose-envify@1.4.0(transitive)
- Removedobject-assign@4.1.1(transitive)
- Removedreact@17.0.2(transitive)
- Removedreact-dom@17.0.2(transitive)
- Removedscheduler@0.20.2(transitive)