Nimiq Accounts Manager
The Accounts Manager (or Nimiq Accounts) provides a unified interface for
all Nimiq accounts, addresses, and contracts. It is the primary UI for Nimiq users
to manage their accounts and provides websites and apps with a concise API to
interact with their users' Nimiq addresses.
The Accounts Client library
Installation
Include the AccountsClient
JS library as a script tag in your page:
<script src="https://unpkg.com/@nimiq/accounts-client@v0.4/dist/standalone/AccountsClient.standalone.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@nimiq/accounts-client@v0.4/dist/standalone/AccountsClient.standalone.umd.js"></script>
It can also be installed from NPM:
npm install @nimiq/accounts-client
yarn add @nimiq/accounts-client
Then import or require it in your module:
import AccountsClient from '@nimiq/accounts-client';
const AccountsClient = require('@nimiq/accounts-client');
Initialization
To start the client, just instantiate the class by passing it the URL of the
Accounts Manager to connect to:
const accountsClient = new AccountsClient('https://accounts.nimiq-testnet.com');
const accountsClient = new AccountsClient('https://accounts.nimiq.com');
Usage
By default, the client opens a popup window for user interactions. On mobile
devices, a new tab will be opened instead. For simplicity, we will always refer
to popups throughout this documentation.
Popups will be blocked if not opened within the context of an active user
action. Thus, it is required that API methods are called synchronously within
the context of a user action, such as a click. See example below.
document.getElementById('checkoutButton').addEventListener('click', function(event){
accountsClient.checkout();
});
For more details about avoiding popup blocking refer to
this article.
Using top-level redirects
Note: To use redirects instead of popups, your app must run under a
HTTPS domain!
If you prefer top-level redirects instead of popups, you can pass an
instance of RedirectRequestBehavior
as a second parameter to either the
AccountsClient initialization or to any API method:
Note: The way to configure top-level redirects will change in an upcoming
version of the Accounts Client!
const redirectBehavior = new AccountsClient.RedirectRequestBehavior();
const accountsClient = new AccountsClient(<url>, redirectBehavior);
const result = accountsClient.checkout(<requestOptions>, redirectBehavior);
The RedirectRequestBehavior
accepts two optional parameters:
The first is the return URL. If no return URL is specified, the current URL
without parameters will be used.
const redirectBehavior = new RedirectRequestBehavior('https://url.to/return?to');
The second optional parameter is a plain object you can use to store data until
the request returns:
const storedData = { foo: 'I am the state' };
const redirectBehavior = new RedirectRequestBehavior(null, storedData);
For details on how to listen for redirect responses and retrieve the stored
data, see Listening for redirect responses.
API Methods
Note:
All API methods run asynchronously and thus return promises. Please keep in
mind that promises can also be rejected for various reasons, e.g. when the user
cancels the request by closing the popup window or clicking on a cancel
button.
An error can also occur when the request contains invalid parameters. The request
promise will be rejected with an Error
object.
Checkout
The checkout()
method allows your site to request a transaction from the user.
This will open a popup for the user to select the address to send from —
or cancel the request. During the payment process, the signed transaction is
sent (relayed) to the network but also returned to the caller, e.g. for
processing in your site, storage on your server or re-submittal.
const requestOptions = {
appName: 'Nimiq Shop',
shopLogoUrl: 'https://your.domain.com/path/to/an/image.jpg',
recipient: 'NQ07 0000 0000 0000 0000 0000 0000 0000 0000',
value: 100 * 1e5,
};
const signedTransaction = await accountsClient.checkout(requestOptions);
The checkout()
method returns a promise which resolves to a
SignedTransaction
:
interface SignedTransaction {
serializedTx: string;
hash: string;
raw: {
signerPublicKey: Uint8Array;
signature: Uint8Array;
sender: string;
senderType: Nimiq.Account.Type;
recipient: string;
recipientType: Nimiq.Account.Type;
value: number;
fee: number;
validityStartHeight: number;
extraData: Uint8Array;
flags: number;
networkId: number;
}
}
The serializedTx
can be handed to a Nimiq JSON-RPC's sendRawTransaction
method.
The raw
object can be handed to the NanoApi's relayTransaction
method.
Choose Address
By using the chooseAddress()
method, you are asking the user to select one of
their addresses to provide to your website. This can be used for example to find
out which address your app should send funds to.
Note: This method should not be used as a login or authentication mechanism,
as it does not provide any security that the user actually owns the provided address!
The method takes a basic request object as its only argument, which must only contain
the appName
property:
const requestOptions = {
appName: 'Nimiq Safe',
};
const address = await accountsClient.chooseAddress(requestOptions);
The request's result contains an address string as address
and a label
:
interface Address {
address: string;
label: string;
}
Sign Transaction
The signTransaction()
method is similar to checkout, but provides a different
UI to the user. The main difference to checkout()
is that it requires the
request to already include the sender's address as sender
, as well as the
transaction's validityStartHeight
. The created transaction will only be
returned to the caller, not sent to the network automatically.
For brevity, most duplicate parameter explanations are omitted here, please
refer to Checkout for more details.
const requestOptions = {
appName: 'Nimiq Safe',
sender: 'NQxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx',
recipient: 'NQxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx',
value: 100 * 1e5,
validityStartHeight: 123456,
};
const signedTransaction = await accountsClient.signTransaction(requestOptions);
The signTransaction()
method returns a SignedTransaction
. See
Checkout for details.
Signup
The signup()
method creates a new account in the Accounts Manager. The user
will choose an Identicon and optionally set a password.
const requestOptions = {
appName: 'Nimiq Safe',
};
const account = await accountsClient.signup(requestOptions);
The signup()
method returns a promise which resolves to an Account
:
interface Account {
accountId: string;
label: string;
type: WalletType;
fileExported: boolean;
wordsExported: boolean;
addresses: Array<{
address: string;
label: string;
}>;
}
Login
The login()
method allows the user to add an existing account to the
Accounts Manager by importing their Login File, Recovery Words or
old Account Access File. After an account has been imported, the
Accounts Manager automatically detects active addresses following the
BIP44
method.
const requestOptions = {
appName: 'Nimiq Safe',
};
const account = await accountsClient.login(requestOptions);
The login()
method returns a promise which resolves to an Account
. Please see
the result type for signup()
for details.
Onboard
The onboard()
method presents a choice menu between Signup, Login, and
Connect Ledger to the user and is thus a general purpose onboarding method.
Just like the direct methods, it only requires a simple request object:
const requestOptions = {
appName: 'Nimiq Safe',
};
const account = await accountsClient.onboard(requestOptions);
Since onboard()
is a wrapper around Signup, Login and Ledger, it also returns an
Account
result type. Please see the result type for signup()
for details.
Logout
The logout()
method removes an account from the Accounts Manager. During the
logout process, the user can export the Login File or Recovery Words before
the account is deleted.
const requestOptions = {
appName: 'Nimiq Safe',
accountId: 'xxxxxxxx',
};
const logoutResult = await accountsClient.logout(requestOptions);
The logout()
method returns a promise which resolves to a simple object
containing the success
property, which is always true:
{ success: true }
Export
Using the export()
method, a user can retrieve the Login File or
Recovery Words of an account.
const requestOptions = {
appName: 'Nimiq Safe',
accountId: 'xxxxxxxx',
};
const exportResult = await accountsClient.export(requestOptions);
The export()
method returns a promise which resolves to an object that
contains flags for each export type:
interface ExportResult {
fileExported: boolean;
wordsExported: boolean;
}
Change Password
With the changePassword()
method, a user can change the password of an account:
const requestOptions = {
appName: 'Nimiq Safe',
accountId: 'xxxxxxxx',
};
const result = await accountsClient.changePassword(requestOptions);
The changePassword()
method returns a promise which resolves to a simple object
containing the success
property, which is always true:
{ success: true }
Add Address
By using the addAddress()
method, the user is able to derive and add an additional
address to their account. The method returns the added address and its label.
The method takes a simple request object as its argument:
const requestOptions = {
appName: 'Nimiq Safe',
accountId: 'xxxxxxxx',
};
const address = await accountsClient.addAddress(requestOptions);
The request's result contains an address string as address
and a label
:
interface Address {
address: string;
label: string;
}
Rename
To rename a user's account or addresses, you can call the rename()
method. The
UI for the rename action always presents the given account and all its addresses
to the user. By sending an optional address with the request, that address's label
will be already pre-selected for the user.
This method takes the following request object as its only argument:
const requestOptions = {
appName: 'Nimiq Safe',
accountId: 'xxxxxxxx',
address: 'NQxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx';
};
const account = await accountsClient.rename(requestOptions);
Since more than one label can be renamed during the rename request, the result
contains the whole account, including all visible addresses. Please see the
result type for signup()
for details about the Account
object.
Sign Message
To let the user sign an arbitrary message with any of their addresses, you can
call signMessage()
with the following request object. If you do not include
a signer
property, the user will be prompted to select an address from their
available accounts. The message can be either a string or a Uint8Array byte array.
const requestOptions = {
appName: 'Nimiq Safe',
message: 'String to sign' || new Uint8Array([...]),
};
const signedMessage = await accountsClient.signMessage(requestOptions);
The method returns a SignedMessage
object containing the following properties:
interface SignedMessage {
signer: string;
signerPublicKey: Uint8Array;
signature: Uint8Array;
}
Note: To prevent users from signing valid transactions or other
blockchain-related proofs which could be used to impersonate them, the
Nimiq Keyguard prefixes additional data to the message before signing.
This prefix consists of
- a 23 bytes prefix (
'\x16Nimiq Signed Message:\n'
, available as AccountsClient.MSG_PREFIX
) - the length of the message as a stringified number
This data is then hashed with SHA256 before being signed. Together, this leads
to the following data structure:
sign( sha256( '\x16Nimiq Signed Message:\n' + message.length + message ) );
Verifying a signed message could go like this:
const signature = new Nimiq.Signature(signedMessage.signature);
const publicKey = new Nimiq.PublicKey(signedMessage.signerPublicKey);
const data = AccountsClient.MSG_PREFIX
+ message.length
+ message;
const dataBytes = Nimiq.BufferUtils.fromUtf8(data);
const hash = Nimiq.Hash.computeSha256(dataBytes);
const isValid = signature.verify(publicKey, hash);
Listening for redirect responses
If you configured the AccountsClient to use
top-level redirects instead of popups, you need to
follow the four steps below to specifically listen for the redirects from the
Accounts Manager back to your site, using the on()
method.
Your handler functions will be called with two parameters: the result object and
the stored data object as it was passed to the
RedirectRequestBehavior
during initialization.
const accountsClient = new AccountsClient();
const onSuccess = function(result, storedData) {
console.log("Got result from Accounts Manager:", result);
console.log("Retrieved stored data:": storedData);
}
const onError = function(error, storedData) {
console.log("Got error from Accounts Manager:", error);
console.log("Retrieved stored data:": storedData);
}
accountsClient.on(AccountsClient.RequestType.CHECKOUT, onSuccess, onError);
accountsClient.on(AccountsClient.RequestType.SIGN_TRANSACTION, onSuccess, onError);
accountsClient.on(AccountsClient.RequestType.LOGIN, onSuccess, onError);
accountsClient.checkRedirectResponse();
The available RequestType
s, corresponding to the API methods, are:
enum AccountsClient.RequestType {
CHECKOUT = 'checkout',
CHOOSE_ADDRESS = 'choose-address',
SIGN_TRANSACTION = 'sign-transaction',
SIGNUP = 'signup',
LOGIN = 'login',
ONBOARD = 'onboard',
LOGOUT = 'logout',
EXPORT = 'export',
CHANGE_PASSWORD = 'change-password',
ADD_ADDRESS = 'add-address',
RENAME = 'rename',
SIGN_MESSAGE = 'sign-message',
}
Running your own Accounts Manager
TODO
If you want to run your own instance of Accounts Manager, you also need to run
an instance of the Keyguard.
Contribute
To get started with working on the source code, pull the code and install the dependencies:
Setup
git clone https://github.com/nimiq/accounts.git
cd accounts
yarn
Run
Compile and serve with hot-reload in the background for development:
yarn run serve
Compile and lint continuously in the background for development:
yarn run build --watch
Lint and fix files:
yarn run lint
Run unit tests:
yarn run test
Build
Compile and minify for production:
yarn run build
Configuration
The following values can be changed via configuration files:
- keyguardEndpoint: The location of your keyguard instance.
- network: The network you want to use. Possible values are 'main', 'test' and
'dev'. You can use the constants (see default configs).
- networkEndpoint: The location of the network iframe instance you want to use.
- privilegedOrigins: An array of origins with special access rights, nameley
permission to use iframe methods like
list()
. - redirectTarget: In case of empty referrer or absence of request, the user is
redirected to this page.
The default config file is config.local.ts
. To use a different file
(especially useful for deployment), set an environment variable
build
. E.g. export build='testnet'
to use config.testnet.ts
. To set
environment variables permanently, please refer to your server's documentation,
e.g. for Apache.