Endless Wallet Standard

Endless Wallet Standard

The wallet standard provides guidelines for interoperability between wallet types. This ensures dapp developers do not need to change their applications to handle different wallets. This standard offers a single interface for all dapp developers, allowing easy additions of new wallets and more users to each application. This interoperability allows users to choose which wallet they want without worrying about whether apps support their use cases.

In order to ensure interoperability across Endless wallets, the following is required:

  1. Mnemonics - a set of words that can be used to derive account private keys

  2. dapp API - entry points into the wallet to support access to identity managed by the wallet

Mnemonics phrases

A mnemonic phrase is a multiple word phrase that can be used to generate account addresses. However, some wallets may want to support one mnemonic to many accounts coming from other chains. To support both of these use cases, the Endless wallet standard uses a Bitcoin Improvement Proposal (BIP44) to derive path for mnemonics to accounts.

Creating an Endless account

Endless account creation can be supported across wallets in the following manner:

  1. Generate a mnemonic phrase, for example with BIP39.

  2. Get the master seed from that mnemonic phrase.

  3. Use the BIP44-derived path to retrieve an account address (m/44'/637'/0'/0'/0')

import { Account, isValidHardenedPath } from '@endlesslab/endless-ts-sdk'
/**
 * Creates new account with bip44 path and mnemonics,
 * @param path. (e.g. m/44'/637'/0'/0'/0')
 * Detailed description: {@link https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki}
 * @param mnemonics.
 * @returns Account
 */
static fromDerivationPath(path: string, mnemonics: string): Account {
  if (!isValidHardenedPath(path)) {
    throw new Error("Invalid derivation path");
  }

  const normalizeMnemonics = mnemonics
    .trim()
    .split(/\s+/)
    .map((part) => part.toLowerCase())
    .join(" ");

  const endlessAccount = Account.fromDerivationPath({
    path: `m/44'/637'/0'/0'/0'`,
    mnemonic: normalizeMnemonics,
  });

  return endlessAccount;
}

Supporting one mnemonic per multiple account wallets

Many wallets from other ecosystems use this paradigm, and take these steps to generate accounts

  1. Generate a mnemonic phrase, for example with BIP39.

  2. Get the master seed from that mnemonic phrase.

  3. Use the BIP44-derived path to retrieve private keys (e.g. m/44'/637'/i'/0'/0') where i is the account index.

  4. Increase i until all the accounts the user wants to import are found.

    • Note: The iteration should be limited, if an account doesn't exist during iteration, keep iterating for a constant address_gap_limit (10 for now) to see if there are any other accounts. If an account is found we will continue to iterate as normal.

ie.

import { EndlessConfig, Network } from '@endlesslab/endless-ts-sdk';

const gapLimit = 10;
let currentGap = 0;

for (let i = 0; currentGap < gapLimit; i += 1) {
  const derivationPath = `m/44'/637'/${i}'/0'/0'`;
  const endlessAccount = fromDerivationPath(derivationPath, mnemonic);
  const endless = new Endless(new EndlessConfig({
    network: Network.TESTNET
  }));

    const response = await endless
    .getAccountResource({
      accountAddress: endlessAccount.accountAddress,
      resourceType: '0x1::account::Account',
    }).catch(error => error);
  if (response.status !== 404) {
    wallet.addAccount(account);
    currentGap = 0;
  } else {
    currentGap += 1;
  }
}

dapp API

More important than account creation, is how wallets connect to dapps. Additionally, following these APIs will allow for the wallet developer to integrate with the Endless Wallet Adapter Standard. The APIs are as follows:

  • connect(), disconnect()

  • getAccount()

  • changeNetwork()

  • getNetwork()

  • signAndSubmitTransaction(data: EndlessSignAndSubmitTransactionInput)

  • signMessage(data: EndlessSignMessageInput)

  • Event listening (EndLessSDKEvent.ACCOUNT_CHANGE, EndLessSDKEvent.SWITCH_NETWORK)

// Common Args and Responses
enum UserResponseStatus {
  APPROVED = 'Approved',
  REJECTED = 'Rejected'
}

interface UserApproval<TResponseArgs> {
  status: UserResponseStatus.APPROVED;
  args: TResponseArgs;
}

interface UserRejection {
  status: UserResponseStatus.REJECTED;
  message?: string;
}

type UserResponse<TResponseArgs> = UserApproval<TResponseArgs> | UserRejection;

type AccountInfo {
    account: AccountAddress;
    address: string;
    authKey: string;
    ansName?: string;
}

type NetworkInfo = {
  name: Network;
  chainId: number;
  url: string;
};

// The important thing to return here is the transaction hash, the dapp can wait for it

// https://github.com/endless-labs/endless-ts-sdk/-/blob/main/src/types/index.ts
type PendingTransactionResponse

// https://www.npmjs.com/package/@endlesslab/endless-web3-sdk/
//      with /src/types.ts
type EndlessSignAndSubmitTransactionInput

// https://github.com/endless-labs/endless-ts-sdk/-/blob/main/src/transactions/types.ts
type InputGenerateTransactionPayloadData

Connection APIs

The connection APIs ensure that wallets don't accept requests until the user acknowledges that they want to see the requests. This keeps the user state clean and prevents the user from unknowingly having prompts.

  • connect() will prompt the user for a connection

    • return Promise<UserResponse<AccountInfo>>

  • disconnect() allows the user to stop giving access to a dapp and also helps the dapp with state management

    • return Promise<void>

State APIs

Get Account

Connection required

Allows a dapp to query for the current connected account address and authkey

  • getAccount() no prompt to the user

    • returns Promise<UserResponse<AccountInfo>>

Get Network

Connection required

Allows a dapp to query for the current connected network name, chain ID, and URL

  • getNetwork() no prompt to the user

    • returns Promise<UserResponse<NetworkInfo>>

Signing APIs

Sign and submit transaction

Connection required

Allows a dapp to send a simple JSON payload using the TypeScript SDK for signing and submission to the current network. The user should be prompted for approval.

  • signAndSubmitTransaction(data: EndlessSignAndSubmitTransactionInput) will prompt the user with the transaction they are signing

    • returns Promise<UserResponse<{ hash: string }>>

Sign message

Connection required

Allows a dapp to sign a message with their private key. The most common use case is to verify identity, but there are a few other possible use cases. The user should be prompted for approval. You may notice some wallets from other chains just provide an interface to sign arbitrary strings. This can be susceptible to man-in-the-middle attacks, signing string transactions, etc.

Types:

export interface EndlessSignMessageInput {
  address?: boolean;
  application?: boolean;
  chainId?: boolean;
  message: string;
  nonce?: string;
}

export type EndlessSignMessageOutput = {
  address?: string;
  application?: string;
  chainId?: number;
  fullMessage: string;
  publicKey: string;
  message: string;
  nonce: string;
  prefix: 'Endless';
  signature: Signature;
};
  • signMessage(data: EndlessSignMessageInput) prompts the user with the payload.message to be signed

    • returns Promise<SignMessageResponse>

An example: signMessage({message: "Welcome to dapp!"})

This would generate the fullMessage to be signed and returned as the signature:

prefix: Endless
address: 0x000001
application: endless.link
chain_id: 221
nonce: 1234034
message: Welcome to dapp!
publicKey: xxx
fullMessage: xxx
signature: xxx

Event listening

  • Event listening

jssdk.onAccountChange((res) => {
  console.log('ACCOUNT_CHANGE', res);
})
jssdk.onNetworkChange((res) => {
  console.log('NETWORK_CHANGE', res);
})
// or
jssdk.on(EndLessSDKEvent.CONNECT, res => {
  console.log('CONNECT', res);
})
jssdk.on(EndLessSDKEvent.ACCOUNT_CHANGE, res => {
  console.log('ACCOUNT_CHANGE', res);
})
jssdk.on(EndLessSDKEvent.NETWORK_CHANGE, res => {
  console.log('NETWORK_CHANGE', res);
})
jssdk.on(EndLessSDKEvent.DISCONNECT, res => {
  console.log('DISCONNECT', res)
})
jssdk.on(EndLessSDKEvent.OPEN, res => {
  console.log('OPEN', res)
})
jssdk.on(EndLessSDKEvent.CLOSE, res => {
  console.log('CLOSE', res)
})

Last updated