Skip to content

Commit

Permalink
Merge pull request #68 from CityOfZion/CU-86dtu8ra6
Browse files Browse the repository at this point in the history
CU-86dtu8ra6 - LedgerService - MultiAccount support
  • Loading branch information
lopescode authored Sep 6, 2024
2 parents 4644fcb + 0c4d05c commit 8e9e253
Show file tree
Hide file tree
Showing 71 changed files with 1,030 additions and 10,467 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cityofzion/blockchain-service",
"comment": "Add to support ledger multi account",
"type": "minor"
}
],
"packageName": "@cityofzion/blockchain-service"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cityofzion/bs-ethereum",
"comment": "Add to support ledger multi account",
"type": "minor"
}
],
"packageName": "@cityofzion/bs-ethereum"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cityofzion/bs-neo-legacy",
"comment": "Add to support ledger multi account",
"type": "minor"
}
],
"packageName": "@cityofzion/bs-neo-legacy"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cityofzion/bs-neo3",
"comment": "Add to support ledger multi account",
"type": "minor"
}
],
"packageName": "@cityofzion/bs-neo3"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cityofzion/bs-react-native-decrypt",
"comment": "Add to support ledger multi account",
"type": "patch"
}
],
"packageName": "@cityofzion/bs-react-native-decrypt"
}
47 changes: 8 additions & 39 deletions packages/blockchain-service/src/BSAggregator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AccountWithDerivationPath, BlockchainService } from './interfaces'
import { fetchAccountsForBlockchainServices } from './functions'
import { Account, BlockchainService } from './interfaces'

export class BSAggregator<
BSCustomName extends string = string,
Expand Down Expand Up @@ -48,44 +49,12 @@ export class BSAggregator<
return this.#blockchainServices.filter(bs => bs.validateEncrypted(keyOrJson)).map(bs => bs.blockchainName)
}

async generateAccountFromMnemonicAllBlockchains(
mnemonic: string,
skippedAddresses?: string[]
): Promise<Map<BSCustomName, AccountWithDerivationPath[]>> {
const mnemonicAccounts = new Map<BSCustomName, AccountWithDerivationPath[]>()

const promises = this.#blockchainServices.map(async service => {
let index = 0
const accounts: AccountWithDerivationPath[] = []
let hasError = false

while (!hasError) {
const generatedAccount = service.generateAccountFromMnemonic(mnemonic, index)
if (skippedAddresses && skippedAddresses.find(address => address === generatedAccount.address)) {
index++
continue
}

if (index !== 0) {
try {
const { transactions } = await service.blockchainDataService.getTransactionsByAddress({
address: generatedAccount.address,
})
if (!transactions || transactions.length <= 0) hasError = true
} catch {
hasError = true
}
}

accounts.push(generatedAccount)
index++
async generateAccountsFromMnemonic(mnemonic: string): Promise<Map<BSCustomName, Account[]>> {
return fetchAccountsForBlockchainServices(
this.#blockchainServices,
async (service: BlockchainService, index: number) => {
return service.generateAccountFromMnemonic(mnemonic, index)
}

mnemonicAccounts.set(service.blockchainName, accounts)
})

await Promise.all(promises)

return mnemonicAccounts
)
}
}
39 changes: 39 additions & 0 deletions packages/blockchain-service/src/functions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Account,
BlockchainService,
BSCalculableFee,
BSClaimable,
Expand Down Expand Up @@ -60,3 +61,41 @@ export async function waitForTransaction(service: BlockchainService, txId: strin

return false
}

export async function fetchAccountsForBlockchainServices<BSCustomName extends string = string>(
blockchainServices: BlockchainService<BSCustomName>[],
getAccountCallback: (service: BlockchainService<BSCustomName>, index: number) => Promise<Account>
): Promise<Map<BSCustomName, Account[]>> {
const accountsByBlockchainService = new Map<BSCustomName, Account[]>()

const promises = blockchainServices.map(async service => {
let index = 0
const accounts: Account[] = []
let shouldBreak = false

while (!shouldBreak) {
const generatedAccount = await getAccountCallback(service, index)

if (index !== 0) {
try {
const { transactions } = await service.blockchainDataService.getTransactionsByAddress({
address: generatedAccount.address,
})

if (!transactions || transactions.length <= 0) shouldBreak = true
} catch {
shouldBreak = true
}
}

accounts.push(generatedAccount)
index++
}

accountsByBlockchainService.set(service.blockchainName, accounts)
})

await Promise.all(promises)

return accountsByBlockchainService
}
12 changes: 5 additions & 7 deletions packages/blockchain-service/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ export type Account = {
key: string
type: 'wif' | 'privateKey' | 'publicKey'
address: string
}
export type AccountWithDerivationPath = Account & {
derivationPath: string
bip44Path?: string
}
export interface Token {
symbol: string
Expand Down Expand Up @@ -42,14 +40,14 @@ export type TransferParam = {

export interface BlockchainService<BSCustomName extends string = string, BSAvailableNetworks extends string = string> {
readonly blockchainName: BSCustomName
readonly derivationPath: string
readonly bip44DerivationPath: string
readonly feeToken: Token
exchangeDataService: ExchangeDataService
blockchainDataService: BlockchainDataService
tokens: Token[]
network: Network<BSAvailableNetworks>
setNetwork: (partialNetwork: Network<BSAvailableNetworks>) => void
generateAccountFromMnemonic(mnemonic: string | string, index: number): AccountWithDerivationPath
generateAccountFromMnemonic(mnemonic: string | string, index: number): Account
generateAccountFromKey(key: string): Account
decrypt(keyOrJson: string, password: string): Promise<Account>
encrypt(key: string, password: string): Promise<string>
Expand Down Expand Up @@ -245,8 +243,8 @@ export type LedgerServiceEmitter = TypedEmitter<{
export interface LedgerService {
emitter: LedgerServiceEmitter
getLedgerTransport?: (account: Account) => Promise<Transport>
getAddress(transport: Transport): Promise<string>
getPublicKey(transport: Transport): Promise<string>
getAccounts(transport: Transport): Promise<Account[]>
getAccount(transport: Transport, index: number): Promise<Account>
}

export type SwapRoute = {
Expand Down
70 changes: 36 additions & 34 deletions packages/bs-ethereum/src/BSEthereum.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
Account,
AccountWithDerivationPath,
BSCalculableFee,
BSWithExplorerService,
BSWithLedger,
Expand All @@ -19,15 +18,16 @@ import { ethers } from 'ethers'
import * as ethersJsonWallets from '@ethersproject/json-wallets'
import * as ethersBytes from '@ethersproject/bytes'
import * as ethersBigNumber from '@ethersproject/bignumber'
import { GhostMarketNDSEthereum } from './GhostMarketNDSEthereum'
import { EthersLedgerServiceEthereum } from './EthersLedgerServiceEthereum'
import { BSEthereumConstants, BSEthereumNetworkId } from './constants/BSEthereumConstants'
import { EthersLedgerServiceEthereum } from './services/ledger/EthersLedgerServiceEthereum'
import Transport from '@ledgerhq/hw-transport'
import { BSEthereumNetworkId, BSEthereumHelper } from './BSEthereumHelper'
import { MoralisBDSEthereum } from './MoralisBDSEthereum'
import { MoralisEDSEthereum } from './MoralisEDSEthereum'
import { BlockscoutNeoXBDSEthereum } from './BlockscoutNeoXBDSEthereum'
import { BlockscoutNeoXEDSEthereum } from './BlockscoutNeoXEDSEthereum'
import { BlockscoutNeoXESEthereum } from './BlockscoutNeoXESEthereum'
import { BSEthereumHelper } from './helpers/BSEthereumHelper'
import { BlockscoutBDSEthereum } from './services/blockchain-data/BlockscoutBDSEthereum'
import { BlockscoutEDSEthereum } from './services/exchange-data/BlockscoutEDSEthereum'
import { MoralisBDSEthereum } from './services/blockchain-data/MoralisBDSEthereum'
import { MoralisEDSEthereum } from './services/exchange-data/MoralisEDSEthereum'
import { GhostMarketNDSEthereum } from './services/nft-data/GhostMarketNDSEthereum'
import { BlockscoutESEthereum } from './services/explorer/BlockscoutESEthereum'

export class BSEthereum<BSCustomName extends string = string>
implements
Expand All @@ -39,7 +39,7 @@ export class BSEthereum<BSCustomName extends string = string>
BSWithExplorerService
{
readonly blockchainName: BSCustomName
readonly derivationPath: string
readonly bip44DerivationPath: string

feeToken!: Token
blockchainDataService!: BlockchainDataService
Expand All @@ -55,37 +55,39 @@ export class BSEthereum<BSCustomName extends string = string>
network?: Network<BSEthereumNetworkId>,
getLedgerTransport?: (account: Account) => Promise<Transport>
) {
network = network ?? BSEthereumHelper.DEFAULT_NETWORK
network = network ?? BSEthereumConstants.DEFAULT_NETWORK

this.blockchainName = blockchainName
this.ledgerService = new EthersLedgerServiceEthereum(getLedgerTransport)
this.derivationPath = BSEthereumHelper.DERIVATION_PATH
this.ledgerService = new EthersLedgerServiceEthereum(this, getLedgerTransport)
this.bip44DerivationPath = BSEthereumConstants.DEFAULT_BIP44_DERIVATION_PATH

this.setNetwork(network)
}

async #buildTransferParams(param: TransferParam) {
async #generateSigner(account: Account, isLedger?: boolean): Promise<ethers.Signer> {
const provider = new ethers.providers.JsonRpcProvider(this.network.url)

let ledgerTransport: Transport | undefined

if (param.isLedger) {
if (isLedger) {
if (!this.ledgerService.getLedgerTransport)
throw new Error('You must provide getLedgerTransport function to use Ledger')
ledgerTransport = await this.ledgerService.getLedgerTransport(param.senderAccount)
}

let signer: ethers.Signer
if (ledgerTransport) {
signer = this.ledgerService.getSigner(ledgerTransport, provider)
} else {
signer = new ethers.Wallet(param.senderAccount.key, provider)
if (typeof account.bip44Path !== 'string') throw new Error('Your account must have bip44 path to use Ledger')

const ledgerTransport = await this.ledgerService.getLedgerTransport(account)
return this.ledgerService.getSigner(ledgerTransport, account.bip44Path, provider)
}

return new ethers.Wallet(account.key, provider)
}

async #buildTransferParams(param: TransferParam) {
const signer = await this.#generateSigner(param.senderAccount, param.isLedger)
if (!signer.provider) throw new Error('Signer must have provider')

const decimals = param.intent.tokenDecimals ?? 18
const amount = ethersBigNumber.parseFixed(param.intent.amount, decimals)

const gasPrice = await provider.getGasPrice()
const gasPrice = await signer.provider.getGasPrice()

let transactionParams: ethers.utils.Deferrable<ethers.providers.TransactionRequest> = {
type: 2,
Expand Down Expand Up @@ -125,16 +127,16 @@ export class BSEthereum<BSCustomName extends string = string>

this.network = network

if (BlockscoutNeoXBDSEthereum.isSupported(network)) {
this.exchangeDataService = new BlockscoutNeoXEDSEthereum(network)
this.blockchainDataService = new BlockscoutNeoXBDSEthereum(network)
if (BlockscoutBDSEthereum.isSupported(network)) {
this.exchangeDataService = new BlockscoutEDSEthereum(network)
this.blockchainDataService = new BlockscoutBDSEthereum(network)
} else {
this.exchangeDataService = new MoralisEDSEthereum(network, this.blockchainDataService)
this.blockchainDataService = new MoralisBDSEthereum(network)
}

this.nftDataService = new GhostMarketNDSEthereum(network)
this.explorerService = new BlockscoutNeoXESEthereum(network)
this.explorerService = new BlockscoutESEthereum(network)
}

validateAddress(address: string): boolean {
Expand Down Expand Up @@ -163,15 +165,15 @@ export class BSEthereum<BSCustomName extends string = string>
return true
}

generateAccountFromMnemonic(mnemonic: string[] | string, index: number): AccountWithDerivationPath {
const path = this.derivationPath.replace('?', index.toString())
const wallet = ethers.Wallet.fromMnemonic(Array.isArray(mnemonic) ? mnemonic.join(' ') : mnemonic, path)
generateAccountFromMnemonic(mnemonic: string[] | string, index: number): Account {
const bip44Path = this.bip44DerivationPath.replace('?', index.toString())
const wallet = ethers.Wallet.fromMnemonic(Array.isArray(mnemonic) ? mnemonic.join(' ') : mnemonic, bip44Path)

return {
address: wallet.address,
key: wallet.privateKey,
type: 'privateKey',
derivationPath: path,
bip44Path,
}
}

Expand Down Expand Up @@ -215,7 +217,7 @@ export class BSEthereum<BSCustomName extends string = string>
try {
gasLimit = await signer.estimateGas(transactionParams)
} catch {
gasLimit = BSEthereumHelper.DEFAULT_GAS_LIMIT
gasLimit = BSEthereumConstants.DEFAULT_GAS_LIMIT
}

const transaction = await signer.sendTransaction({
Expand Down
Loading

0 comments on commit 8e9e253

Please sign in to comment.