Electron / TypeScript
The reference for @keygrant/electron 0.9.0, the KeyGrant SDK for Electron and Node: every
option, call and status. The rules every SDK shares are in
How every SDK behaves.
- Node.js 20 or later.
- Runs in Electron's main process or any Node process, not in a renderer: it uses Node's file system, child processes and WebCrypto.
- Depends on
zodonly. Nothing in it is specific to Electron.
Add it
@keygrant/electron is on npm.
npm install @keygrant/electron Use it
The quick start, after Add it: embed your product's public key, activate a key, and check the license on every launch. You need a product with a license model and a key to try (see Getting started).
1. Get the public key
The SDK verifies each lease offline with your product's public keys. Fetch them once and ship them with your app. The request needs no API key:
curl https://api.keygrant.dev/v1/products/my-product/pubkey { "publicJwk": { "kty": "OKP", "key_ops": ["verify"], "alg": "EdDSA", "ext": true, "crv": "Ed25519", "x": "…" }, "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "…", "kid": "…" } ]} -
my-productis your product's API ID, the last part of the product page's URL in the dashboard (/app/products/my-product). -
Pass the
keysarray to the SDK'spublicJwksoption unchanged: it is every key a build of your app should carry, each with its id (kid). - Copy key set, on the product's Signing key tab, copies the same set as
{"keys": [...]}. -
The SPKI and
.pemforms, and when the keys change, are in Get your product's public key.
2. Activate a key
Create one License for the process, and activate the key your customer enters:
import { License } from "@keygrant/electron";
const license = new License({ product: "my-product", apiBaseUrl: "https://api.keygrant.dev", publicJwks: PUBLIC_KEYS, // the keys from step 1 major: 1, // this build's major version});
const result = await license.activate(userEnteredKey);if (!result.ok) showActivationError(result.error); -
activatesends the key to KeyGrant to be checked, takes one of the key's device seats for this machine, and stores the signed lease KeyGrant returns, a JWT that your app verifies offline. -
When it fails,
result.errorsays why, such asinvalid-keyorlimit. Every value is in Activating.
3. Check the license on launch
// Run on every launch.async function checkLicense() { const status = await license.status(); if (status.state === "licensed" || status.state === "trial") return; // run if (status.state === "unknown") { // The license file could not be read: keep running and check again shortly. setTimeout(checkLicense, 5_000); return; } // "expired" or "invalid": status.reason says why. "unlicensed": no key is stored yet. showActivationWindow(status);}
checkLicense(); status() verifies the stored lease on the device, and usually needs no network.
- A lease lasts the key's Offline days, and never runs past the key's expiry plus its grace period: see How long a lease lasts.
-
A device trial started with
startTrial()gets one lease that runs to the trial's end. -
When the lease nears its end,
status()renews it first, waiting at most 5 seconds for KeyGrant: see When the SDK goes online. - When KeyGrant cannot be reached, your app keeps working until the lease runs out. See Offline validation & grace.
The rest of this page is every option, call and status.
new License(options)
The constructor checks the public keys and revokedLicenseFiles, and no other option, and
does no I/O. It throws when:
- neither
publicJwksnorpublicJwkis given, or both are; publicJwksis empty;-
a key is not an Ed25519 public JWK: its
ktyis notOKP, itscrvnotEd25519, it holds the privated, itsuseis notsig, itskey_opsdo not includeverify, itsalgis notEdDSAorEd25519, or itsxis not 32 bytes of base64url; revokedLicenseFilesis given and is not a list of texts.
An x pasted with its padding is read as the same key.
Required. The product's API ID, as it appears in the request path.
Required. Set it to https://api.keygrant.dev. A single trailing slash is removed.
The product's public keys, an array of JWKs used to verify leases offline: the keys from /pubkey. Give this or publicJwk. See Rotating the signing key.
- A lease names the key that signed it (
kid) and is verified by that key. - A lease signed before leases named their key names none, and is verified by each key in turn.
One Ed25519 public key as a JWK: a key set of one, as an app configured before key sets has it. Give this or publicJwks.
The major version of this build. The SDK uses its whole number (2.7 is 2). See Licensing by version.
- Anything below 1, or anything that is not a finite number, reads as 1, as KeyGrant reads a 0.x version, so a 0.x build can set 0 or 1.
- KeyGrant rejects a major above 99,999, and
activate()then fails withnetwork.
What the license's device list calls this machine. Sent on activation, truncated to 120 characters.
false turns device hold off, so no license is held to its device. See Device management.
How long one request may take, in milliseconds. The SDK enforces this limit even when a custom HTTP adapter does not.
- The checks
status()makes while the lease is still valid have their own 5-second limit.
The ids of the license files this build refuses (each file's fid), matched exactly: how an app update stops an offline license file that leaked. See License files.
- A stored file listed here reads as
invalidwith reasonrevoked-file, and importing one is refused withrevoked-file.
Your own storage, registry, clock, fingerprint or http, to replace the defaults. See Adapters.
Methods
Calls on one License run one at a time, except has, require,
upgradeUrl and the two pickUp calls, which only read and run beside any call.
Create one License per process.
Activates the key on this device and stores its lease. Resolves { ok: true } or { ok: false, error } (below). The key is trimmed and uppercased first.
optionsis{ email?, code? }, for a key whose license model locks who can activate (below). Any other key ignores it, and nothing in it is stored.- Unlike
status(), it does not use the stored lease: it contacts KeyGrant every time, unless the key cannot be valid, the device cannot be identified, or an earlier call is stuck.
Resolves whether the license grants the entitlement code: a flag that is on, a number that is not zero, or a text value that is not empty. The code is matched exactly, case included. See Check an entitlement.
- Answered offline from what the device holds, as
status()would answer it, with no request and no write. So a license that does not license this build grants nothing, and a running trial grants what its trial lease does. - It never rejects: an unreadable license file grants nothing.
Like has(code), but resolves nothing, and rejects with MissingEntitlementError, whose entitlement is code, when the license does not grant it.
Takes the content of a .kglicense file the customer was sent: text, a byte array (a Node Buffer too) or an ArrayBuffer, never a path. Resolves { ok: true, kind, licensedTo } or { ok: false, error } (License files).
- It verifies the file offline and stores an offline file. It activates a key file's key as
activate()does, andoptionsis then the same{ email?, code? }.
Drops the stored offline license file, if there is one. A key this device holds keeps working, and deactivate() leaves the file alone. Resolves nothing.
- Rejects when an earlier call has not finished or when the change cannot be saved.
Spends n uses (1 by default) of the key's meter meter, before the work they count. Resolves { ok: true, left } or { ok: false, error } and never rejects (Metered uses).
Asks KeyGrant to email a six-digit code to the address, for key pickup. Resolves { ok: true, sent: true } whether or not the address has a key, or { ok: false, error }. Stores nothing, and never rejects.
Resolves { ok: true, keys }, the address's live keys of the product for the code that came, or { ok: false, error }. Stores nothing, and never rejects.
- Each key is a
PickedKey:key,model,modelName,state,entitlements,expiresAt,versions,deviceLimit,activeDevices,held(this device already holds a seat on it),saleandactivationCode.
Confirms the code and activates the key that fits this build, never handing it to your app: the one this device holds a seat on, else one with a free seat, else the first. Resolves { ok: true } or { ok: false, error }.
Verifies what the device holds, offline first, and resolves a status. It contacts KeyGrant only in the cases listed in Offline validation & grace.
Like status(), but contacts KeyGrant immediately, even during the wait after a failed check-in. Call it when the customer says they have paid. status(true) is the same call.
- It contacts KeyGrant only when a license key is stored, or when a device trial whose lease no longer verifies still has time left.
- If KeyGrant does not respond, it resolves the status from what the device holds.
Starts this device's trial with KeyGrant and resolves the trial's status: trial, expired with reason trial, or unlicensed. It resolves unknown when the license file cannot be read. See Trials.
Releases this device's seat and removes the license from this device. Resolves { released }, explained below.
Your Stripe Payment Link with client_reference_id set to this device's activation ID (never the key), which names the key to upgrade (How KeyGrant finds the key). See Sell an upgrade.
- Resolves
nullwhen no key and activation are stored, without readinglink. - Otherwise it rejects when
linkis not an absolute URL. It also rejects when the license file cannot be read.
Activating
- Activating the same key again on the same device keeps its seat.
- Activating a different key replaces the stored license without releasing the old key's seat: call
deactivate()first to free it. activate()rejects when the license file cannot be read or the activation cannot be saved.
activate() answers one of:
The key is empty or longer than 120 characters (no request is made), or KeyGrant does not recognize it.
The key is already active on as many devices as its device limit allows, or more after its limit was lowered.
The key is revoked, or past its expiry and grace period.
This build is a newer major version than the key covers.
This build is an older major version than the key covers.
KeyGrant returned a lease that does not verify on this device, for example one signed by a key this build does not carry. KeyGrant has assigned the seat, but nothing is stored on the device.
KeyGrant did not respond (offline, or a timeout), or returned any other response.
- Any other response: a server error, a rate limit, a product ID it does not know, an archived product (on a device that holds no seat on the key), or a request it rejected as malformed.
activate()also fails with it, without a request, while an earlier call is stuck, or when the device cannot be identified, such as when this device activates the same key again and cannot read its machine ID in time.
Keys only their buyer can activate
When a key is locked to its buyer (Who can activate), pass
what the customer typed as activate(key, { email, code }), asking for each only when an
error names it. The steps and rules are in
How every SDK behaves. Since 0.7.0.
let result = await license.activate(key)if (!result.ok && result.error === 'user-email-required') { // Ask for the email the key was sold to. result = await license.activate(key, { email })}if (!result.ok && result.error === 'user-code-required') { // A code was just emailed to that address: ask for it. result = await license.activate(key, { email, code })}if (!result.ok && result.error === 'try-again') { // The code could not be emailed. Nothing was activated; offer to try again.} - The
optionsargument comes in 0.7.0: in 0.6.0 and earlier,activate(key)takes none, and answers a locked keynetwork. importLicenseFile(content, { email, code })passes them on to a key file's key.
The errors it answers:
The key is locked to its buyer and this device is new: ask for the email the key was sold to and call activate(key, { email }).
That is not the email the key was sold to (or it is longer than any email, and no request is made). KeyGrant never says what the right one is.
The email was right and KeyGrant has just emailed a code to it: ask for the code and call activate(key, { email, code }).
The code is wrong, expired (it lasts 10 minutes) or already used, or is not six digits (no request is made). Ask again, or call activate(key, { email }) for a new code.
Three codes have been emailed for this key this hour. Use the last one, or wait.
Too many wrong emails or codes for this key this hour. Try again later.
The code could not be emailed just now, and nothing was activated. Try again; it is nobody's mistake.
Restoring a license without the key
For an App Store build, which may not take a typed key: pickUp(email) emails the customer a
six-digit code, and restoreByEmail(email, code) activates the key that fits this build.
How a pickup works is in How every SDK behaves. Since 0.9.0.
// An App Store build: no key field. Ask for the email the customer bought with.await license.pickUp(email)// They type the code from the email:const result = await license.restoreByEmail(email, code)if (!result.ok && result.error === 'wrong-code') { // Ask for the code again.} else if (!result.ok && result.error === 'code-expired') { // Offer to send a new code: license.pickUp(email).} else if (!result.ok && result.error === 'no-key') { // No purchase on that email.} -
To let the customer choose, call
pickUp(email, code), show each key by itsmodelName, and activate the one chosen withactivate(picked.key, { email, code: picked.activationCode }). restoreByEmailwith the same email and code afterpickUp(email, code), or again after an activation that got no answer, needs no new code (What is kept).- The requests are on the endpoints page.
The errors a pickup answers:
No email, one longer than any email (no request is made), or one KeyGrant could not read.
Not the code KeyGrant emailed, or not six digits (no request is made, and it costs no try). Ask again.
The address has no live code: it expired (it lasts 10 minutes), was used, was voided by its fifth wrong try, or was never asked for. Call pickUp(email) for a new one.
Thirty wrong codes for the address this hour, or too many requests from this network. retryAfter says how many seconds to wait, when KeyGrant gives it.
The product's Key pickup is off.
KeyGrant could not take the request just now. Try again.
KeyGrant did not respond, or returned something that is not an answer.
From restoreByEmail(): the address has no live key of the product. Its other errors are upgrade and outdated (the address has keys, none for this build) and activate()'s own.
Deactivation
deactivate() resolves { released }:
released | When |
|---|---|
true | KeyGrant confirmed the release. |
true | Nothing was stored. |
true | The stored license is held to another machine's ID (for example a copied or restored profile). The seat stays with the other machine, and KeyGrant is not contacted. |
false | KeyGrant could not confirm the release, because it did not respond or returned an error. Tell the customer the seat may still be in use. |
- A copied license that is not held to a machine ID is released like any other.
- In every case the key, the activation and the lease are removed from this device. The record of any device trial is kept, and so is a stored offline license file (
removeLicenseFile()drops that). deactivate()rejects when an earlier call is stuck. It also rejects when the license file cannot be read or saved, and the key then stays on the device.
The status
status(), refresh() and startTrial() resolve the same status object.
It does not include the license's expiry date or grace period. A trial status includes the trial's end.
One of six states, listed below.
Why, on an expired, invalid or unknown status.
The stored license key.
- Absent on
trial,unlicensedandunknown, onexpiredwith reasontrial, and oninvalidwith reasontampered. - On a status from an offline license file (
source, below): the key the file records, onlicensedand oninvalidwith reasonrevoked-file,upgradeoroutdated; absent on any other.
On trial: when it ends, in Unix milliseconds.
On trial: whole days left, rounded up.
true on a licensed status from a test license. Absent, not false, otherwise.
On licensed and trial, when the lease grants at least one: a frozen object of flags, numbers and text values. See Editions & entitlements.
On licensed, from a key's lease that names meters: the key's meters by code, each { left, total, offline, unsynced, resets } (below). Answered offline. Absent on every other status.
'file' when the status rests on a stored offline license file (below), licensed or not. Absent for a key's lease, a trial and no license. A status from a file has no seat to deactivate.
Who the stored offline license file is licensed to, as it signs it: { name?, email? }, to show as "Licensed to Ada Lovelace".
- On a status from a file that is
licensed, orinvalidwith reasonrevoked-file,upgradeoroutdated; absent otherwise.
true when the status was read from storage without changes, because an earlier call has not finished. Restarting the app clears it.
States and reasons
| state | reason | Means |
|---|---|---|
licensed | A valid lease for this key, this device and this build. | |
trial | A valid trial lease. | |
expired | offline | The lease has run out and has not been renewed, because KeyGrant did not respond, returned a response that neither renews nor refuses the key (a server error or a rate limit, for example), or has not been contacted again yet. |
expired | trial | The trial has ended, or its lease no longer verifies here. |
invalid | revoked | KeyGrant refused the key because it was revoked, it is past its expiry and grace period, or this device's seat was released. |
invalid | upgrade | This build is a newer major version than the key covers. |
invalid | outdated | This build is an older major version than the key covers. |
invalid | device | The license was activated on another machine. |
invalid | tampered | The lease does not verify, or is for another product, key or activation. |
invalid | unknown-key | The lease was signed by a key this build does not carry, such as one the product switched to after this build was made. See Rotating the signing key. |
invalid | revoked-file | The stored offline license file is one this build lists as revoked (revokedLicenseFiles). See License files. |
unlicensed | No key and no trial is stored. | |
unknown | storage | The license file could not be read. Keep running as before and check again shortly. Do not show the activation screen for this status. |
status() never rejects because of a storage error: an unreadable file gives
unknown, and if a save fails, the status still reflects KeyGrant's response. What to
tell the customer for each reason is in
Offline validation & grace.
License files
A customer can be sent a .kglicense file: a key file, which activates its key,
or an offline license file, which licenses the app with no network. How each behaves is in
How every SDK behaves, and what each costs under
Delivered as in License models. Since
0.6.0.
Warning
Keep every key that signed a file in publicJwks. A build without it reads such a file as
invalid with reason unknown-key.
LICENSE_FILE_EXTENSIONis.kglicense, for an open dialog's filter or a drop handler's test.importLicenseFiletakes the file's content, never its path, so your app reads the file and hands over its bytes.- List the ids of leaked files in
revokedLicenseFiles(Blocking a leaked file). - These are not the file the SDK stores its own state in (below).
// main processimport { app, dialog, ipcMain } from 'electron'import { readFile } from 'node:fs/promises'import { License, LICENSE_FILE_EXTENSION } from '@keygrant/electron'
const license = new License({ product: 'sluice', apiBaseUrl: 'https://api.keygrant.dev', publicJwks: KEYS.keys, major: 3, revokedLicenseFiles: [], // ids of files that leaked: an update adds them here})
// The renderer hands over a dropped file's bytes.ipcMain.handle('license:import', (_event, bytes: Uint8Array) => license.importLicenseFile(bytes))
// "Open license file..." in a menu.async function openLicenseFile() { const { filePaths } = await dialog.showOpenDialog({ filters: [{ name: 'License file', extensions: [LICENSE_FILE_EXTENSION.slice(1)] }], }) if (filePaths[0]) return license.importLicenseFile(await readFile(filePaths[0]))}
// macOS: a .kglicense the app is registered for, double-clicked in Finder.app.on('open-file', async (event, path) => { event.preventDefault() try { const result = await license.importLicenseFile(await readFile(path)) if (!result.ok) showError(result.error) } catch (error) { showError(error) // unreadable, not saved, or a license call still running: try again }}) A dropped file reaches the main process from the renderer through the preload script:
// preloadimport { contextBridge, ipcRenderer } from 'electron'contextBridge.exposeInMainWorld('licensing', { importFile: (bytes: Uint8Array) => ipcRenderer.invoke('license:import', bytes),})
// rendererwindow.addEventListener('dragover', (event) => event.preventDefault())window.addEventListener('drop', async (event) => { event.preventDefault() const file = event.dataTransfer?.files[0] if (!file) return const result = await window.licensing.importFile(new Uint8Array(await file.arrayBuffer())) if (result.ok) showThanks(result.licensedTo.name ?? result.licensedTo.email) else showError(result.error)}) What importLicenseFile answers
It verifies the file offline (Importing a file) and resolves
{ ok: true, kind: 'key' | 'offline', licensedTo }, or
{ ok: false, error } with nothing stored. error is one of these, or, for a
key file whose key did not activate, whatever activate() answers
(above).
Not a license file's text, or its token or claims are broken.
A signed token, but not a license file (a lease, say).
Not signed by the key it names.
Signed by a key this build does not carry (publicJwks): a newer one, after the product rotated its key, which an update of the app carries. A key file's notes print its key, which can be typed meanwhile.
Another product's file.
The file's id is listed in revokedLicenseFiles.
An offline file that does not cover this build: this build is newer than the file covers.
An offline file that does not cover this build: this build is older than the file covers.
- It rejects when an earlier call on the
Licensehas not finished, when an offline file cannot be saved, and, for a key file, whereveractivate()rejects. - A key file of a locked key takes the same
{ email, code }asactivate()(above), and an offline file ignores it.
The status of a stored file
A stored offline file is checked again on every status(), which then resolves
{ state: 'licensed', source: 'file', key, licensedTo, ... } with the file's
entitlements, and never asks KeyGrant, refresh() included.
- Show "Licensed to Ada Lovelace" from
licensedTo. - A status with
source: 'file'has no seat to deactivate: offerremoveLicenseFile()instead.deactivate()leaves the file alone. - Which wins when the device also holds a key, and the reasons a file's status can be
invalid, are in The status of a stored file.
Metered uses
A license model can count uses of what it sells (see Metered uses).
The app spends them with use(meter, n), before the work they count, and does the work only
when the spend is ok. Since 0.8.0.
const spent = await license.use('exports') // use('exports', 40) spends forty at once, whole or noneif (spent.ok) { runExport() // spent.left: what is left now, net of this device's unsynced spends} else if (spent.error === 'used-up') { offerMoreExports(spent.left) // KeyGrant's word: fewer left than asked (0 when none)} else if (spent.error === 'offline-limit') { askToConnect() // KeyGrant could not be reached, and this is past what this device may spend without it} else { showMeterProblem(spent.error) // 'no-meter', 'not-licensed', 'revoked', 'device', 'upgrade', 'outdated', 'storage', 'invalid-amount'}
const { meters } = await license.status() // offline, from the leasemeters?.exports // { left: 950, total: 1000, offline: 100, unsynced: 0, resets: false } use()resolves{ ok: true, left }, withoffline: truewhen the device spent from its offline allowance, or{ ok: false, error }. It never rejects.- A request waits at most 5 seconds for KeyGrant, less when
httpTimeoutMsis shorter. deactivate()sends the unsynced spends with the release.- How a spend works online and offline, and why a meter never takes the license away, is in How every SDK behaves.
When use() does not spend
The meter has fewer uses left than asked. left says how many are left (0 when none), so the app can offer fewer. It is KeyGrant's own word, not a guess.
KeyGrant could not be reached, and the spend is past this device's offline allowance for the meter or past what it last heard is left. left says what the device may still spend. Ask the customer to connect.
The key has no meter with that code.
No key's lease licenses this build on this device, so nothing was asked. A device trial and an offline license file have no meters.
KeyGrant's verdict on the key, this build or this device, as status() says it.
The license file could not be read just now. Try again shortly.
n is not a whole number from 1 to 1,000,000,000. This is a bug in the app: nothing was asked.
status().meters
By meter code, on a licensed status from a key's lease that names meters. It is a plain
object: to test a code that is also an Object.prototype member, use
Object.hasOwn(status.meters ?? {}, code).
What is left: the uses the lease said were left, less what this device has spent since and not reported. Never below 0.
The uses the key holds, for good or for this billing period, with any added on top.
How many uses this device may spend while KeyGrant cannot be reached.
Uses this device spent that KeyGrant has not heard of yet.
true for a meter that comes back to its full uses each billing period.
Adapters
Pass any of them in adapters; the SDK uses the default for each one you leave out.
The default fingerprinter has 2 seconds to read the machine ID during a status check, and 10
seconds during an activation or a trial.
interface StorageAdapter { // readOnly: write nothing, not even a repair. read(opts?: { readOnly?: boolean }): Promise<StoredState | null>; write(state: StoredState): Promise<void>; // Optional. When present, every save goes through it. update?(merge: (latest: StoredState) => StoredState): Promise<void>;}
interface RegistryStash { readTrialStart(): Promise<number | null>; writeTrialStart(ms: number): Promise<void>;}
interface Clock { now(): number; // Unix milliseconds}
interface Fingerprinter { get(): Promise<string>; // Used when present. Only kind "machine" holds a license to its device. identify?(opts: { timeoutMs: number }): Promise<DeviceIdentity>;}
interface DeviceIdentity { fingerprint: string | null; kind?: "machine" | "hostname"; alternates: string[];}
interface HttpAdapter { // Must resolve, not throw, on a 4xx. post( url: string, body: unknown, opts?: { timeoutMs?: number }, ): Promise<{ status: number; json: unknown }>;} -
fingerprint: a custom fingerprinter holds a license to its machine only if it implementsidentifyand reportskind: "machine". If it implements onlyget(), licenses are never held to the device. -
registry: the default stores nothing. A registry stash you supply is read when a trial starts, and the earliest start date it holds is sent with the request. The start date KeyGrant returns is then written back to it. http: if your adapter throws, the SDK treats it as no response.
Where the license is stored
The default storage is one JSON file, <product>.json, in the per-user folder every SDK
shares. The folders, and how a save works, are in
Where the license is stored. defaultStoragePath(product)
gives its path, and createFileStorage(path) keeps it somewhere else (below).
Other exports
The default storage, at a path you choose. options.migrateFrom is an older path: whenever the file at the new path is missing, the state at the older path is copied to it. The older file is left in place.
Where the default storage keeps <product>.json (below).
The default HTTP adapter, on the global fetch. options.timeoutMs is its fallback timeout.
The default fingerprinter: the operating system's machine ID, hashed. See Device management.
The host name fingerprinter: the host name, platform and architecture, hashed.
The SHA-256, in hex, of keygrant| followed by raw. The default fingerprint is this hash of machine-id| and the machine ID.
The default registry stash, which stores nothing.
The default clock, Date.now().
15000, the default httpTimeoutMs.
.kglicense, the extension of a license file, for an open dialog's filter.
Whether a status's entitlements grant code, by the rule has(code) uses, for a status already in hand.
What require(code) rejects with. Its entitlement is the code.
The package also exports these types:
LicenseConfig,LicenseStatus,LicenseState,LicenseReason,LicenseError,ActivateResult,DeactivateResult,Adapters,StorageAdapter,StoredState,RegistryStash,Clock,Fingerprinter,DeviceIdentity,HttpAdapter,EntitlementandEntitlements.- In 0.6.0:
ImportLicenseFileResult,LicenseFileError,LicenseFileKindandLicensedTo. - In 0.7.0:
ActivateOptions. - In 0.8.0:
MeterStatus,UseResult,UseError,MeterFigureandSpendEntry. - In 0.9.0:
PickedKey,PickupError,PickUpSentResult,PickUpKeysResultandRestoreResult.