@tetherto/wdk
Version:
A flexible manager that can register and manage multiple wallet instances for different blockchains dynamically.
295 lines (231 loc) • 9.4 kB
JavaScript
// Copyright 2024 Tether Operations Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict'
import WalletManager from '@tetherto/wdk-wallet'
import { SwapProtocol, BridgeProtocol, LendingProtocol } from '@tetherto/wdk-wallet/protocols'
/** @typedef {import('@tetherto/wdk-wallet').IWalletAccount} IWalletAccount */
/** @typedef {import('@tetherto/wdk-wallet').FeeRates} FeeRates */
/** @typedef {import('./wallet-account-with-protocols.js').IWalletAccountWithProtocols} IWalletAccountWithProtocols */
/** @typedef {<A extends IWalletAccount>(account: A) => Promise<void>} MiddlewareFunction */
export default class WDK {
/**
* Creates a new wallet development kit instance.
*
* @param {string | Uint8Array} seed - The wallet's BIP-39 seed phrase.
* @throws {Error} If the seed is not valid.
*/
constructor (seed) {
if (!WDK.isValidSeed(seed)) {
throw new Error('Invalid seed.')
}
/** @private */
this._seed = seed
/** @private */
this._wallets = new Map()
/** @private */
this._protocols = { swap: { }, bridge: { }, lending: { } }
/** @private */
this._middlewares = { }
}
/**
* Returns a random BIP-39 seed phrase.
*
* @returns {string} The seed phrase.
*/
static getRandomSeedPhrase () {
return WalletManager.getRandomSeedPhrase()
}
/**
* Checks if a seed is valid.
*
* @param {string | Uint8Array} seed - The seed.
* @returns {boolean} True if the seed is valid.
*/
static isValidSeed (seed) {
if (seed instanceof Uint8Array) {
return seed.length >= 16 && seed.length <= 64
}
return WalletManager.isValidSeedPhrase(seed)
}
/**
* Registers a new wallet to WDK.
*
* @template {typeof WalletManager} W
* @param {string} blockchain - The name of the blockchain the wallet must be bound to. Can be any string (e.g., "ethereum").
* @param {W} WalletManager - The wallet manager class.
* @param {ConstructorParameters<W>[1]} config - The configuration object.
* @returns {WDK} The wdk instance.
*/
registerWallet (blockchain, WalletManager, config) {
const wallet = new WalletManager(this._seed, config)
this._wallets.set(blockchain, wallet)
return this
}
/**
* Registers a new protocol to WDK.
*
* The label must be unique in the scope of the blockchain and the type of protocol (i.e., there can't be two protocols of the
* same type bound to the same blockchain with the same label).
*
* @see {@link IWalletAccountWithProtocols#registerProtocol} to register protocols only for specific accounts.
* @template {typeof SwapProtocol | typeof BridgeProtocol | typeof LendingProtocol} P
* @param {string} blockchain - The name of the blockchain the protocol must be bound to. Can be any string (e.g., "ethereum").
* @param {string} label - The label.
* @param {P} Protocol - The protocol class.
* @param {ConstructorParameters<P>[1]} config - The protocol configuration.
* @returns {WDK} The wdk instance.
*/
registerProtocol (blockchain, label, Protocol, config) {
if (Protocol.prototype instanceof SwapProtocol) {
this._protocols.swap[blockchain] ??= { }
this._protocols.swap[blockchain][label] = { Protocol, config }
} else if (Protocol.prototype instanceof BridgeProtocol) {
this._protocols.bridge[blockchain] ??= { }
this._protocols.bridge[blockchain][label] = { Protocol, config }
} else if (Protocol.prototype instanceof LendingProtocol) {
this._protocols.lending[blockchain] ??= { }
this._protocols.lending[blockchain][label] = { Protocol, config }
}
return this
}
/**
* Registers a new middleware to WDK.
*
* It's possible to register multiple middlewares for the same blockchain, which will be called sequentially.
*
* @param {string} blockchain - The name of the blockchain the middleware must be bound to. Can be any string (e.g., "ethereum").
* @param {MiddlewareFunction} middleware - A callback function that is called each time the user derives a new account.
* @returns {WDK} The wdk instance.
*/
registerMiddleware (blockchain, middleware) {
this._middlewares[blockchain] ??= []
this._middlewares[blockchain].push(middleware)
return this
}
/**
* Returns the wallet account for a specific blockchain and index (see BIP-44).
*
* @param {string} blockchain - The name of the blockchain (e.g., "ethereum").
* @param {number} [index] - The index of the account to get (default: 0).
* @returns {Promise<IWalletAccountWithProtocols>} The account.
* @throws {Error} If no wallet has been registered for the given blockchain.
*/
async getAccount (blockchain, index = 0) {
if (!this._wallets.has(blockchain)) {
throw new Error(`No wallet registered for blockchain: ${blockchain}.`)
}
const wallet = this._wallets.get(blockchain)
const account = await wallet.getAccount(index)
await this._runMiddlewares(account, { blockchain })
this._registerProtocols(account, { blockchain })
return account
}
/**
* Returns the wallet account for a specific blockchain and BIP-44 derivation path.
*
* @param {string} blockchain - The name of the blockchain (e.g., "ethereum").
* @param {string} path - The derivation path (e.g., "0'/0/0").
* @returns {Promise<IWalletAccountWithProtocols>} The account.
* @throws {Error} If no wallet has been registered for the given blockchain.
*/
async getAccountByPath (blockchain, path) {
if (!this._wallets.has(blockchain)) {
throw new Error(`No wallet registered for blockchain: ${blockchain}.`)
}
const wallet = this._wallets.get(blockchain)
const account = await wallet.getAccountByPath(path)
await this._runMiddlewares(account, { blockchain })
this._registerProtocols(account, { blockchain })
return account
}
/**
* Returns the current fee rates for a specific blockchain.
*
* @param {string} blockchain - The name of the blockchain (e.g., "ethereum").
* @returns {Promise<FeeRates>} The fee rates (in base unit).
* @throws {Error} If no wallet has been registered for the given blockchain.
*/
async getFeeRates (blockchain) {
if (!this._wallets.has(blockchain)) {
throw new Error(`No wallet registered for blockchain: ${blockchain}.`)
}
const wallet = this._wallets.get(blockchain)
const feeRates = await wallet.getFeeRates()
return feeRates
}
/**
* Disposes and unregisters all the wallets, erasing any sensitive data from the memory.
*/
dispose () {
for (const [, wallet] of this._wallets) {
wallet.dispose()
}
this._wallets.clear()
}
/** @private */
async _runMiddlewares (account, { blockchain }) {
if (this._middlewares[blockchain]) {
for (const middleware of this._middlewares[blockchain]) {
await middleware(account)
}
}
}
/** @private */
_registerProtocols (account, { blockchain }) {
const protocols = { swap: { }, bridge: { }, lending: { } }
account.registerProtocol = (label, Protocol, config) => {
if (Protocol.prototype instanceof SwapProtocol) {
protocols.swap[label] = new Protocol(account, config)
} else if (Protocol.prototype instanceof BridgeProtocol) {
protocols.bridge[label] = new Protocol(account, config)
} else if (Protocol.prototype instanceof LendingProtocol) {
protocols.lending[label] = new Protocol(account, config)
}
return account
}
account.getSwapProtocol = (label) => {
if (this._protocols.swap[blockchain]?.[label]) {
const { Protocol, config } = this._protocols.swap[blockchain][label]
const protocol = new Protocol(account, config)
return protocol
}
if (protocols.swap[label]) {
return protocols.swap[label]
}
throw new Error(`No swap protocol registered for label: ${label}.`)
}
account.getBridgeProtocol = (label) => {
if (this._protocols.bridge[blockchain]?.[label]) {
const { Protocol, config } = this._protocols.bridge[blockchain][label]
const protocol = new Protocol(account, config)
return protocol
}
if (protocols.bridge[label]) {
return protocols.bridge[label]
}
throw new Error(`No bridge protocol registered for label: ${label}.`)
}
account.getLendingProtocol = (label) => {
if (this._protocols.lending[blockchain]?.[label]) {
const { Protocol, config } = this._protocols.lending[blockchain][label]
const protocol = new Protocol(account, config)
return protocol
}
if (protocols.lending[label]) {
return protocols.lending[label]
}
throw new Error(`No lending protocol registered for label: ${label}.`)
}
}
}