Skip to content

Commit

Permalink
Merge pull request #134 from CityOfZion/CU-86a53cg51
Browse files Browse the repository at this point in the history
CU-86a53cg51 - BSLib - Add support to Neo Legacy app
  • Loading branch information
thiagocbalducci authored Dec 20, 2024
2 parents 593dd6c + bf47b24 commit 56679e9
Show file tree
Hide file tree
Showing 9 changed files with 453 additions and 3,713 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cityofzion/bs-neo-legacy",
"comment": "Add Ledger support",
"type": "minor"
}
],
"packageName": "@cityofzion/bs-neo-legacy"
}
3,830 changes: 128 additions & 3,702 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions packages/bs-neo-legacy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@
"@cityofzion/blockchain-service": "workspace:*",
"@cityofzion/bs-asteroid-sdk": "workspace:*",
"@cityofzion/dora-ts": "0.0.11",
"@cityofzion/neon-js": "4.8.3"
"@cityofzion/neon-js": "4.8.3",
"@ledgerhq/hw-transport": "~6.30.5"
},
"devDependencies": {
"@types/jest": "29.5.3",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@ledgerhq/hw-transport-node-hid": "~6.28.5",
"dotenv": "16.3.1",
"eslint": "^8.48.0",
"jest": "29.6.2",
"ts-jest": "29.1.1",
"ts-node": "10.9.1",
"typescript": "4.9.5"
}
}
}
32 changes: 31 additions & 1 deletion packages/bs-neo-legacy/src/__tests__/BSNeoLegacy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { generateMnemonic } from '@cityofzion/bs-asteroid-sdk'
import { BSNeoLegacy } from '../services/BSNeoLegacy'
import { BSNeoLegacyConstants, BSNeoLegacyNetworkId } from '../constants/BSNeoLegacyConstants'
import { Network } from '@cityofzion/blockchain-service'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'

let bsNeoLegacy: BSNeoLegacy
let bsNeoLegacy: BSNeoLegacy<'neoLegacy'>

const network: Network<BSNeoLegacyNetworkId> = {
id: 'testnet',
Expand Down Expand Up @@ -187,4 +188,33 @@ describe('BSNeoLegacy', () => {

expect(transactionHash).toEqual(expect.any(String))
})

it.skip('Should be able to transfer with ledger', async () => {
const transport = await TransportNodeHid.create()

const service = new BSNeoLegacy('neoLegacy', undefined, async () => transport)

const account = await service.ledgerService.getAccount(transport, 0)

const balance = await service.blockchainDataService.getBalance(account.address)

const gasBalance = balance.find(b => b.token.symbol === service.feeToken.symbol)

expect(Number(gasBalance?.amount)).toBeGreaterThan(0.00000001)

const [transactionHash] = await service.transfer({
senderAccount: account,
intents: [
{
amount: '0.00000001',
receiverAddress: 'Ac9hjKxteW3BvDyrhTxizkUxEShT8B4DaU',
tokenHash: service.feeToken.hash,
tokenDecimals: service.feeToken.decimals,
},
],
})
transport.close()

expect(transactionHash).toEqual(expect.any(String))
}, 60000)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Transport from '@ledgerhq/hw-transport'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import { NeonJsLedgerServiceNeoLegacy } from '../services/ledger/NeonJsLedgerServiceNeoLegacy'
import { BSNeoLegacy } from '../services/BSNeoLegacy'
import { BSNeoLegacyConstants } from '../constants/BSNeoLegacyConstants'

let ledgerService: NeonJsLedgerServiceNeoLegacy<'neo-legacy'>
let transport: Transport
let bsNeoLegacy: BSNeoLegacy<'neo-legacy'>

describe.skip('NeonJsLedgerServiceNeoLegacy', () => {
beforeAll(async () => {
const network = BSNeoLegacyConstants.TESTNET_NETWORKS[0]!
bsNeoLegacy = new BSNeoLegacy('neo-legacy', network)

transport = await TransportNodeHid.create()
ledgerService = new NeonJsLedgerServiceNeoLegacy(bsNeoLegacy, async () => transport)
}, 60000)

it('Should be able to get all accounts', async () => {
const accounts = await ledgerService.getAccounts(transport)
expect(accounts.length).toBeGreaterThan(1)

accounts.forEach((account, index) => {
expect(account).toEqual(
expect.objectContaining({
address: expect.any(String),
key: expect.any(String),
type: 'publicKey',
bip44Path: bsNeoLegacy.bip44DerivationPath.replace('?', index.toString()),
})
)
})
}, 60000)

it('Should be able to get account', async () => {
const account = await ledgerService.getAccount(transport, 0)
expect(account).toEqual(
expect.objectContaining({
address: expect.any(String),
key: expect.any(String),
type: 'publicKey',
bip44Path: bsNeoLegacy.bip44DerivationPath.replace('?', '0'),
})
)
}, 60000)
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ export class BSNeoLegacyConstants {

static RPC_LIST_BY_NETWORK_ID: Record<BSNeoLegacyNetworkId, string[]> = {
mainnet: [
'http://seed9.ngd.network:10332',
'https://mainnet1.neo2.coz.io:443',
'https://mainnet2.neo2.coz.io:443',
'https://mainnet3.neo2.coz.io:443',
'http://seed9.ngd.network:10332',
'http://seed1.ngd.network:10332',
'http://seed2.ngd.network:10332',
'http://seed3.ngd.network:10332',
Expand Down
2 changes: 2 additions & 0 deletions packages/bs-neo-legacy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export * from './services/blockchain-data/DoraBDSNeoLegacy'
export * from './services/exchange-data/CryptoCompareEDSNeoLegacy'

export * from './services/explorer/NeoTubeESNeoLegacy'

export * from './services/ledger/NeonJsLedgerServiceNeoLegacy'
60 changes: 53 additions & 7 deletions packages/bs-neo-legacy/src/services/BSNeoLegacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
TransferParam,
BSWithExplorerService,
ExplorerService,
BSWithLedger,
GetLedgerTransport,
} from '@cityofzion/blockchain-service'
import { api, sc, u, wallet } from '@cityofzion/neon-js'
import { keychain } from '@cityofzion/bs-asteroid-sdk'
Expand All @@ -18,9 +20,14 @@ import { BSNeoLegacyHelper } from '../helpers/BSNeoLegacyHelper'
import { CryptoCompareEDSNeoLegacy } from './exchange-data/CryptoCompareEDSNeoLegacy'
import { DoraBDSNeoLegacy } from './blockchain-data/DoraBDSNeoLegacy'
import { NeoTubeESNeoLegacy } from './explorer/NeoTubeESNeoLegacy'
import { NeonJsLedgerServiceNeoLegacy } from './ledger/NeonJsLedgerServiceNeoLegacy'

export class BSNeoLegacy<BSName extends string = string>
implements BlockchainService<BSName, BSNeoLegacyNetworkId>, BSClaimable<BSName>, BSWithExplorerService
implements
BlockchainService<BSName, BSNeoLegacyNetworkId>,
BSClaimable<BSName>,
BSWithExplorerService,
BSWithLedger<BSName>
{
readonly name: BSName
readonly bip44DerivationPath: string
Expand All @@ -31,21 +38,45 @@ export class BSNeoLegacy<BSName extends string = string>

blockchainDataService!: BlockchainDataService & BDSClaimable
exchangeDataService!: ExchangeDataService
ledgerService: NeonJsLedgerServiceNeoLegacy<BSName>
explorerService!: ExplorerService
tokens!: Token[]
network!: Network<BSNeoLegacyNetworkId>
legacyNetwork: string

constructor(name: BSName, network?: Network<BSNeoLegacyNetworkId>) {
constructor(name: BSName, network?: Network<BSNeoLegacyNetworkId>, getLedgerTransport?: GetLedgerTransport<BSName>) {
network = network ?? BSNeoLegacyConstants.DEFAULT_NETWORK

this.name = name
this.legacyNetwork = BSNeoLegacyConstants.LEGACY_NETWORK_BY_NETWORK_ID[network.id]
this.ledgerService = new NeonJsLedgerServiceNeoLegacy(this, getLedgerTransport)
this.bip44DerivationPath = BSNeoLegacyConstants.DEFAULT_BIP44_DERIVATION_PATH

this.setNetwork(network)
}

async #generateSigningCallback(account: Account<BSName>) {
const neonJsAccount = new wallet.Account(account.key)

if (account.isHardware) {
if (!this.ledgerService.getLedgerTransport)
throw new Error('You must provide a getLedgerTransport function to use Ledger')

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 {
neonJsAccount,
signingCallback: this.ledgerService.getSigningCallback(ledgerTransport, account),
}
}

return {
neonJsAccount,
}
}

#setTokens(network: Network<BSNeoLegacyNetworkId>) {
const tokens = BSNeoLegacyHelper.getTokens(network)

Expand Down Expand Up @@ -101,6 +132,19 @@ export class BSNeoLegacy<BSName extends string = string>
return { address, key, type, blockchain: this.name }
}

generateAccountFromPublicKey(publicKey: string): Account<BSName> {
if (!wallet.isPublicKey(publicKey)) throw new Error('Invalid public key')

const account = new wallet.Account(publicKey)

return {
address: account.address,
key: account.publicKey,
type: 'publicKey',
blockchain: this.name,
}
}

async decrypt(encryptedKey: string, password: string): Promise<Account<BSName>> {
const key = await wallet.decrypt(encryptedKey, password)
return this.generateAccountFromKey(key)
Expand All @@ -110,9 +154,9 @@ export class BSNeoLegacy<BSName extends string = string>
return wallet.encrypt(key, password)
}

async transfer({ intents, senderAccount, tipIntent, ...params }: TransferParam): Promise<string[]> {
async transfer({ intents, senderAccount, tipIntent, ...params }: TransferParam<BSName>): Promise<string[]> {
const { neonJsAccount, signingCallback } = await this.#generateSigningCallback(senderAccount)
const apiProvider = new api.neoCli.instance(this.network.url)
const account = new wallet.Account(senderAccount.key)
const priorityFee = Number(params.priorityFee ?? 0)

const nativeIntents: ReturnType<typeof api.makeIntent> = []
Expand All @@ -132,7 +176,7 @@ export class BSNeoLegacy<BSName extends string = string>
}

nep5ScriptBuilder.emitAppCall(tokenHashFixed, 'transfer', [
u.reverseHex(wallet.getScriptHashFromAddress(account.address)),
u.reverseHex(wallet.getScriptHashFromAddress(neonJsAccount.address)),
u.reverseHex(wallet.getScriptHashFromAddress(intent.receiverAddress)),
sc.ContractParam.integer(
new u.Fixed8(intent.amount)
Expand All @@ -147,20 +191,22 @@ export class BSNeoLegacy<BSName extends string = string>

if (nep5ScriptBuilder.isEmpty()) {
response = await api.sendAsset({
account,
account: neonJsAccount,
api: apiProvider,
url: this.network.url,
intents: nativeIntents,
fees: priorityFee,
signingFunction: signingCallback,
})
} else {
response = await api.doInvoke({
intents: nativeIntents.length > 0 ? nativeIntents : undefined,
account,
account: neonJsAccount,
api: apiProvider,
script: nep5ScriptBuilder.str,
url: this.network.url,
fees: priorityFee,
signingFunction: signingCallback,
})
}

Expand Down
Loading

0 comments on commit 56679e9

Please sign in to comment.