Try   HackMD

LIfecycle of a wallet API

Keyring Builder

Every wallet must export a keyringBuilder. This function will be called and must return a keyring. Type of keyringBuilder is defined below:

export type keyringBuilder = {
  (): Keyring;
  uid: string;
};

uid is used as a global unique identifier for a keyringBuilder. For example a simple-account's keyringBuilder is described as:

export const SimpleAccountKeyringBuilder = () => {
  return new SimpleAccountKeyring();
};

SimpleAccountKeyringBuilder.uid = 'npm:@epf-wallet/simple-account';

You can use keyringBuilder to pass custom properties in the constructor of your keyring or call init functions if you want to.

Deserialize

Once the keyring is returned from the keyringBuilder, we will next be calling the deserialize function with the data the last stored state of the keyring in local storage. We call the serialize function to get the latest state on keyring and store it in local storage. Click here to know more about serialize funtion.

deserialize: (data: any) => Promise<void>;

Serialize

serialize function is called whenever one of the below listed functions occcus:

  1. New vault is created
  2. New keyring is added
  3. New account is added
  4. An account is removed
  5. keyring is removed
  6. keyring-controller is unmounted or stopped

Everytime the function serialise is called, the keyring must return the latest state of keyring that must be stored in the localstorage so that keyring can be initialised with this state later in time using deserialize function.

serialize: () => Promise<any>;

Add an account

A custom user field input might be needed whenever an account is added to a keyring. For this the keyring must expose the function defineNewAccountView. The input fields returned by this will be displayed to the user and user input will be passed on to the function addAccount.

type KeyringInputError = {
    error: boolean;
    message: string;
}

type KeyringViewInputField = {
    type: 'input' | 'QR' | 'number' | '...etc';
    name: string; // any unique name for this input
    displayName: string;
    displayDescription: string;
    placeholder: string;
    isValid: (value: any) => Promise<KeyringInputError>;
    defaultValue: () => Promise<value>;
}

type KeyringView = {
    title: string;
    description: string;
    inputs: Array<KeyringViewInputField>;
}

// defineNewAccountView defination
// No custom user input will be required if the function returns undefined.
defineNewAccountView: () => Promise<KeyringView | undefined>;
type KeyringViewUserInput = {
    // name key is KeyringViewInputField.name
    [name: string]: any;
}

addAccount: (userInputs: KeyringViewUserInput, chainId: number) => Promise<string> // public address

Wallet must store KeyringViewUserInput internally and may return it in serialised state, as this input will never be sent again to the keyring. When the function getInitCode will be called for a specific address, then the keyring must use the internally stored userInputState to generate a valid initCode.

Get init code

The function getInitCodeAndNonce will always be called after addAccount has been called and it must return the deployment initcode with the default nonce value that must be sent in the user operation. No custom input will be sent to this function, keyring must store the custom input corresponding to an address in the addAccount function. This is becuase the wallet may not be deployed during the initiation phase and maybe deployed later with the first transaction.

type InitCodeAndNonce = {
    initCode: Bytes | string;
    nonce: BigNumber | Bytes | bigint | string | number;
}

getInitCodeAndNonce: (publicAddress: string) => Promise<InitCodeAndNonce>;

Get userOperation signature

A custom user field input might be needed whenever a signature is needed for a transaction. For this the keyring must expose the function defineGetSignatureView. The input fields returned by this will be displayed to the user and user input will be passed on to the function signUserOperation.

// defineGetSignatureView defination
// No custom user input will be required if the function returns undefined.
defineGetSignatureView: (partialUserOp: Partial<UserOperationStruct>) => Promise<KeyringView | undefined>;

We will be passing the partialUserOp to the function where the keyring can take a look at the callData and decide which inputs to take from the user.

The above entered values will then be passed to the signUserOperation function.

signUserOperation: (partialUserOp: Partial<UserOperationStruct>, userInputs: KeyringViewUserInput, chainId: number) => Promise<UserOperationStruct>