@cityofzion/bs-neo-legacy 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,202 +0,0 @@
1
- import {
2
- Account,
3
- BDSClaimable,
4
- BlockchainDataService,
5
- BlockchainService,
6
- BSClaimable,
7
- ExchangeDataService,
8
- Token,
9
- Network,
10
- PartialBy,
11
- TransferParam,
12
- AccountWithDerivationPath,
13
- BSWithExplorerService,
14
- ExplorerService,
15
- } from '@cityofzion/blockchain-service'
16
- import { api, sc, u, wallet } from '@cityofzion/neon-js'
17
- import {
18
- DEFAULT_URL_BY_NETWORK_TYPE,
19
- DERIVATION_PATH,
20
- LEGACY_NETWORK_BY_NETWORK_TYPE,
21
- NATIVE_ASSETS,
22
- TOKENS,
23
- } from './constants'
24
- import { DoraBDSNeoLegacy } from './DoraBDSNeoLegacy'
25
- import { CryptoCompareEDSNeoLegacy } from './CryptoCompareEDSNeoLegacy'
26
- import { keychain } from '@cityofzion/bs-asteroid-sdk'
27
- import { DoraESNeoLegacy } from './DoraESNeoLegacy'
28
-
29
- export class BSNeoLegacy<BSCustomName extends string = string>
30
- implements BlockchainService, BSClaimable, BSWithExplorerService
31
- {
32
- readonly blockchainName: BSCustomName
33
- readonly feeToken: Token
34
- readonly claimToken: Token
35
- readonly burnToken: Token
36
- readonly derivationPath: string
37
-
38
- blockchainDataService!: BlockchainDataService & BDSClaimable
39
- exchangeDataService!: ExchangeDataService
40
- explorerService!: ExplorerService
41
- tokens: Token[]
42
- network!: Network
43
- legacyNetwork: string
44
-
45
- constructor(blockchainName: BSCustomName, network: PartialBy<Network, 'url'>) {
46
- if (network.type === 'custom') throw new Error('Custom network is not supported for NEO Legacy')
47
-
48
- this.blockchainName = blockchainName
49
- this.legacyNetwork = LEGACY_NETWORK_BY_NETWORK_TYPE[network.type]
50
- this.derivationPath = DERIVATION_PATH
51
- this.tokens = TOKENS[network.type]
52
- this.claimToken = this.tokens.find(token => token.symbol === 'GAS')!
53
- this.burnToken = this.tokens.find(token => token.symbol === 'NEO')!
54
- this.feeToken = this.tokens.find(token => token.symbol === 'GAS')!
55
- this.setNetwork(network)
56
- }
57
-
58
- setNetwork(param: PartialBy<Network, 'url'>) {
59
- if (param.type === 'custom') throw new Error('Custom network is not supported for NEO Legacy')
60
-
61
- const network = {
62
- type: param.type,
63
- url: param.url ?? DEFAULT_URL_BY_NETWORK_TYPE[param.type],
64
- }
65
- this.network = network
66
- this.blockchainDataService = new DoraBDSNeoLegacy(network, this.feeToken, this.claimToken)
67
- this.exchangeDataService = new CryptoCompareEDSNeoLegacy(network.type)
68
- this.explorerService = new DoraESNeoLegacy(network.type)
69
- }
70
-
71
- validateAddress(address: string): boolean {
72
- return wallet.isAddress(address)
73
- }
74
-
75
- validateEncrypted(key: string): boolean {
76
- return wallet.isNEP2(key)
77
- }
78
-
79
- validateKey(key: string): boolean {
80
- return wallet.isWIF(key) || wallet.isPrivateKey(key)
81
- }
82
-
83
- generateAccountFromMnemonic(mnemonic: string[] | string, index: number): AccountWithDerivationPath {
84
- keychain.importMnemonic(Array.isArray(mnemonic) ? mnemonic.join(' ') : mnemonic)
85
- const path = this.derivationPath.replace('?', index.toString())
86
- const childKey = keychain.generateChildKey('neo', path)
87
- const key = childKey.getWIF()
88
- const { address } = new wallet.Account(key)
89
- return { address, key, type: 'wif', derivationPath: path }
90
- }
91
-
92
- generateAccountFromKey(key: string): Account {
93
- const type = wallet.isWIF(key) ? 'wif' : wallet.isPrivateKey(key) ? 'privateKey' : undefined
94
- if (!type) throw new Error('Invalid key')
95
-
96
- const { address } = new wallet.Account(key)
97
- return { address, key, type }
98
- }
99
-
100
- async decrypt(encryptedKey: string, password: string): Promise<Account> {
101
- let BsReactNativeDecrypt: any
102
-
103
- try {
104
- const { NativeModules } = require('react-native')
105
- BsReactNativeDecrypt = NativeModules.BsReactNativeDecrypt
106
-
107
- if (!BsReactNativeDecrypt) {
108
- throw new Error('@CityOfZion/bs-react-native-decrypt is not installed')
109
- }
110
- } catch {
111
- const key = await wallet.decrypt(encryptedKey, password)
112
- return this.generateAccountFromKey(key)
113
- }
114
-
115
- const privateKey = await BsReactNativeDecrypt.decryptNeoLegacy(encryptedKey, password)
116
- return this.generateAccountFromKey(privateKey)
117
- }
118
-
119
- encrypt(key: string, password: string): Promise<string> {
120
- return wallet.encrypt(key, password)
121
- }
122
-
123
- async transfer({ intent: transferIntent, senderAccount, tipIntent, ...params }: TransferParam): Promise<string> {
124
- const apiProvider = new api.neoCli.instance(this.network.url)
125
- const account = new wallet.Account(senderAccount.key)
126
- const priorityFee = Number(params.priorityFee ?? 0)
127
-
128
- const nativeIntents: ReturnType<typeof api.makeIntent> = []
129
- const nep5ScriptBuilder = new sc.ScriptBuilder()
130
-
131
- const intents = [transferIntent, ...(tipIntent ? [tipIntent] : [])]
132
-
133
- for (const intent of intents) {
134
- const tokenHashFixed = intent.tokenHash.replace('0x', '')
135
-
136
- const nativeAsset = NATIVE_ASSETS.find(asset => asset.hash === tokenHashFixed)
137
- if (nativeAsset) {
138
- nativeIntents.push(...api.makeIntent({ [nativeAsset.symbol]: Number(intent.amount) }, intent.receiverAddress))
139
- continue
140
- }
141
-
142
- nep5ScriptBuilder.emitAppCall(tokenHashFixed, 'transfer', [
143
- u.reverseHex(wallet.getScriptHashFromAddress(account.address)),
144
- u.reverseHex(wallet.getScriptHashFromAddress(intent.receiverAddress)),
145
- sc.ContractParam.integer(
146
- new u.Fixed8(intent.amount)
147
- .div(Math.pow(10, 8 - (intent.tokenDecimals ?? 8)))
148
- .toRawNumber()
149
- .toString()
150
- ),
151
- ])
152
- }
153
-
154
- let response
155
-
156
- if (nep5ScriptBuilder.isEmpty()) {
157
- response = await api.sendAsset({
158
- account,
159
- api: apiProvider,
160
- url: this.network.url,
161
- intents: nativeIntents,
162
- fees: priorityFee,
163
- })
164
- } else {
165
- response = await api.doInvoke({
166
- intents: nativeIntents.length > 0 ? nativeIntents : undefined,
167
- account,
168
- api: apiProvider,
169
- script: nep5ScriptBuilder.str,
170
- url: this.network.url,
171
- fees: priorityFee,
172
- })
173
- }
174
-
175
- if (!response.tx) throw new Error('Failed to send transaction')
176
- return response.tx.hash
177
- }
178
-
179
- async claim(account: Account): Promise<string> {
180
- const neoAccount = new wallet.Account(account.key)
181
-
182
- const balances = await this.blockchainDataService.getBalance(account.address)
183
- const neoBalance = balances.find(balance => balance.token.symbol === 'NEO')
184
- if (!neoBalance) throw new Error('It is necessary to have NEO to claim')
185
-
186
- const unclaimed = await this.blockchainDataService.getUnclaimed(account.address)
187
- if (Number(unclaimed) <= 0) throw new Error(`Doesn't have gas to claim`)
188
-
189
- const apiProvider = new api.neoCli.instance(this.legacyNetwork)
190
- const claims = await apiProvider.getClaims(account.address)
191
-
192
- const response = await api.claimGas({
193
- claims,
194
- api: apiProvider,
195
- account: neoAccount,
196
- url: this.network.url,
197
- })
198
-
199
- if (!response.tx) throw new Error('Failed to claim')
200
- return response.tx.hash
201
- }
202
- }
@@ -1,50 +0,0 @@
1
- import { Currency, ExchangeDataService, NetworkType, TokenPricesResponse } from '@cityofzion/blockchain-service'
2
- import axios, { AxiosInstance } from 'axios'
3
- import { TOKENS } from './constants'
4
-
5
- type CryptoCompareDataResponse = {
6
- RAW: {
7
- [symbol: string]: {
8
- [currency: string]: {
9
- PRICE: number
10
- }
11
- }
12
- }
13
- }
14
-
15
- export class CryptoCompareEDSNeoLegacy implements ExchangeDataService {
16
- networkType: NetworkType
17
- private axiosInstance: AxiosInstance
18
-
19
- constructor(network: NetworkType) {
20
- this.networkType = network
21
- this.axiosInstance = axios.create({ baseURL: 'https://min-api.cryptocompare.com' })
22
- }
23
-
24
- async getTokenPrices(currency: Currency): Promise<TokenPricesResponse[]> {
25
- if (this.networkType !== 'mainnet') throw new Error('Exchange is only available on mainnet')
26
-
27
- const tokens = TOKENS[this.networkType]
28
- const tokenSymbols = tokens.map(token => token.symbol)
29
- const { data: prices } = await this.axiosInstance.get<CryptoCompareDataResponse>('/data/pricemultifull', {
30
- params: {
31
- fsyms: tokenSymbols.join(','),
32
- tsyms: currency,
33
- },
34
- })
35
-
36
- return Object.entries(prices.RAW)
37
- .map(([symbol, priceObject]) => {
38
- const price = priceObject[currency].PRICE
39
- const token = tokens.find(token => token.symbol === symbol)
40
- if (!token || !price) return
41
-
42
- return {
43
- symbol,
44
- price,
45
- hash: token?.hash,
46
- }
47
- })
48
- .filter((price): price is TokenPricesResponse => price !== undefined)
49
- }
50
- }
@@ -1,176 +0,0 @@
1
- import {
2
- BalanceResponse,
3
- BlockchainDataService,
4
- ContractResponse,
5
- TransactionsByAddressParams,
6
- TransactionsByAddressResponse,
7
- TransactionResponse,
8
- BDSClaimable,
9
- TransactionTransferAsset,
10
- Token,
11
- Network,
12
- } from '@cityofzion/blockchain-service'
13
- import { api } from '@cityofzion/dora-ts'
14
- import { TOKENS } from './constants'
15
- import { rpc } from '@cityofzion/neon-js'
16
-
17
- export class DoraBDSNeoLegacy implements BlockchainDataService, BDSClaimable {
18
- readonly network: Network
19
- private readonly claimToken: Token
20
- private readonly feeToken: Token
21
- private readonly tokenCache: Map<string, Token> = new Map()
22
-
23
- maxTimeToConfirmTransactionInMs: number = 1000 * 60 * 2
24
-
25
- constructor(network: Network, feeToken: Token, claimToken: Token) {
26
- if (network.type === 'custom') throw new Error('Custom network is not supported for NEO Legacy')
27
- this.network = network
28
- this.claimToken = claimToken
29
- this.feeToken = feeToken
30
- }
31
-
32
- async getTransaction(hash: string): Promise<TransactionResponse> {
33
- const data = await api.NeoLegacyREST.transaction(hash, this.network.type)
34
- if (!data || 'error' in data) throw new Error(`Transaction ${hash} not found`)
35
-
36
- const vout: any[] = data.vout ?? []
37
-
38
- const promises = vout.map<Promise<TransactionTransferAsset>>(async (transfer, _index, array) => {
39
- const token = await this.getTokenInfo(transfer.asset)
40
- return {
41
- amount: String(transfer.value),
42
- from: array[array.length - 1]?.address,
43
- contractHash: transfer.asset,
44
- to: transfer.address,
45
- type: 'token',
46
- token,
47
- }
48
- })
49
- const transfers = await Promise.all(promises)
50
-
51
- return {
52
- hash: data.txid,
53
- block: data.block,
54
- fee: (Number(data.sys_fee ?? 0) + Number(data.net_fee ?? 0)).toFixed(this.feeToken.decimals),
55
- time: Number(data.time),
56
- notifications: [], //neoLegacy doesn't have notifications
57
- transfers,
58
- }
59
- }
60
-
61
- async getTransactionsByAddress({
62
- address,
63
- page = 1,
64
- }: TransactionsByAddressParams): Promise<TransactionsByAddressResponse> {
65
- const data = await api.NeoLegacyREST.getAddressAbstracts(address, page, this.network.type)
66
- const transactions = new Map<string, TransactionResponse>()
67
-
68
- const promises = data.entries.map(async entry => {
69
- if (entry.address_from !== address && entry.address_to !== address) return
70
-
71
- const token = await this.getTokenInfo(entry.asset)
72
- const transfer: TransactionTransferAsset = {
73
- amount: String(entry.amount),
74
- from: entry.address_from ?? 'Mint',
75
- to: entry.address_to ?? 'Burn',
76
- type: 'token',
77
- contractHash: entry.asset,
78
- token,
79
- }
80
- const existingTransaction = transactions.get(entry.txid)
81
- if (existingTransaction) {
82
- existingTransaction.transfers.push(transfer)
83
- return
84
- }
85
-
86
- transactions.set(entry.txid, {
87
- block: entry.block_height,
88
- hash: entry.txid,
89
- time: entry.time,
90
- transfers: [transfer],
91
- notifications: [],
92
- })
93
- })
94
- await Promise.all(promises)
95
-
96
- return {
97
- totalCount: data.total_entries,
98
- limit: data.page_size,
99
- transactions: Array.from(transactions.values()),
100
- }
101
- }
102
-
103
- async getContract(contractHash: string): Promise<ContractResponse> {
104
- const response = await api.NeoLegacyREST.contract(contractHash, this.network.type)
105
- if (!response || 'error' in response) throw new Error(`Contract ${contractHash} not found`)
106
-
107
- return {
108
- hash: response.hash,
109
- name: response.name,
110
- methods: [],
111
- }
112
- }
113
-
114
- async getTokenInfo(tokenHash: string): Promise<Token> {
115
- const localToken = TOKENS[this.network.type].find(token => token.hash === tokenHash)
116
- if (localToken) return localToken
117
-
118
- if (this.tokenCache.has(tokenHash)) {
119
- return this.tokenCache.get(tokenHash)!
120
- }
121
-
122
- const data = await api.NeoLegacyREST.asset(tokenHash, this.network.type)
123
- if (!data || 'error' in data) throw new Error(`Token ${tokenHash} not found`)
124
-
125
- const token = {
126
- decimals: Number(data.decimals),
127
- symbol: data.symbol,
128
- hash: data.scripthash,
129
- name: data.name,
130
- }
131
-
132
- this.tokenCache.set(tokenHash, token)
133
-
134
- return token
135
- }
136
-
137
- async getBalance(address: string): Promise<BalanceResponse[]> {
138
- const data = await api.NeoLegacyREST.balance(address, this.network.type)
139
-
140
- const promises = data.map<Promise<BalanceResponse>>(async balance => {
141
- const hash = balance.asset.replace('0x', '')
142
-
143
- let token: Token = {
144
- hash,
145
- name: balance.asset_name,
146
- symbol: balance.symbol,
147
- decimals: 8,
148
- }
149
-
150
- try {
151
- token = await this.getTokenInfo(hash)
152
- } catch {
153
- // Empty block
154
- }
155
-
156
- return {
157
- amount: Number(balance.balance).toFixed(token.decimals),
158
- token,
159
- }
160
- })
161
-
162
- const result = await Promise.all(promises)
163
-
164
- return result
165
- }
166
-
167
- async getUnclaimed(address: string): Promise<string> {
168
- const { unclaimed } = await api.NeoLegacyREST.getUnclaimed(address, this.network.type)
169
- return (unclaimed / 10 ** this.claimToken.decimals).toFixed(this.claimToken.decimals)
170
- }
171
-
172
- async getBlockHeight(): Promise<number> {
173
- const rpcClient = new rpc.RPCClient(this.network.url)
174
- return await rpcClient.getBlockCount()
175
- }
176
- }
@@ -1,18 +0,0 @@
1
- import { BuildNftUrlParams, ExplorerService, NetworkType } from '@cityofzion/blockchain-service'
2
-
3
- export class DoraESNeoLegacy implements ExplorerService {
4
- private networkType: NetworkType
5
-
6
- constructor(networkType: NetworkType) {
7
- this.networkType = networkType
8
- }
9
-
10
- buildTransactionUrl(hash: string): string {
11
- if (this.networkType === 'custom') throw new Error('DoraESNeoLegacy does not support custom network')
12
- return `https://dora.coz.io/transaction/neo2/${this.networkType}/${hash}`
13
- }
14
-
15
- buildNftUrl(_params: BuildNftUrlParams): string {
16
- throw new Error('DoraESNeoLegacy does not support nft')
17
- }
18
- }
@@ -1,123 +0,0 @@
1
- import { BDSClaimable, BlockchainDataService } from '@cityofzion/blockchain-service'
2
- import { DoraBDSNeoLegacy } from '../DoraBDSNeoLegacy'
3
- import { DEFAULT_URL_BY_NETWORK_TYPE, TOKENS } from '../constants'
4
-
5
- const gasToken = TOKENS.testnet.find(t => t.symbol === 'GAS')!
6
- const doraBDSNeoLegacy = new DoraBDSNeoLegacy(
7
- { type: 'testnet', url: DEFAULT_URL_BY_NETWORK_TYPE.testnet },
8
- gasToken,
9
- gasToken
10
- )
11
-
12
- describe('BDSNeoLegacy', () => {
13
- it.each([doraBDSNeoLegacy])('Should be able to get transaction - %s', async (bdsNeoLegacy: BlockchainDataService) => {
14
- const hash = '0x6632e79b1e5182355bcc1f3ca0e91d11a426c893734cd266e7bf3d3f74618add'
15
- const transaction = await bdsNeoLegacy.getTransaction(hash)
16
- expect(transaction).toEqual(
17
- expect.objectContaining({
18
- block: expect.any(Number),
19
- hash,
20
- notifications: [],
21
- transfers: expect.arrayContaining([
22
- expect.objectContaining({
23
- amount: expect.any(String),
24
- from: expect.any(String),
25
- to: expect.any(String),
26
- type: 'token',
27
- }),
28
- ]),
29
- time: expect.any(Number),
30
- fee: expect.any(String),
31
- })
32
- )
33
- })
34
-
35
- it.each([doraBDSNeoLegacy])(
36
- 'Should be able to get history transactions - %s',
37
- async (bdsNeoLegacy: BlockchainDataService) => {
38
- const address = 'AeGgZTTWPzyVtNiQRcpngkV75Xip1hznmi'
39
- try {
40
- const response = await bdsNeoLegacy.getTransactionsByAddress({ address, page: 1 })
41
- response.transactions.forEach(transaction => {
42
- expect(transaction).toEqual(
43
- expect.objectContaining({
44
- block: expect.any(Number),
45
- hash: expect.any(String),
46
- notifications: expect.arrayContaining([
47
- expect.objectContaining({
48
- eventName: expect.any(String),
49
- state: expect.arrayContaining([
50
- {
51
- type: expect.any(String),
52
- value: expect.any(String),
53
- },
54
- ]),
55
- }),
56
- ]),
57
- transfers: expect.arrayContaining([
58
- expect.objectContaining({
59
- amount: expect.any(Number),
60
- from: expect.any(String),
61
- to: expect.any(String),
62
- type: 'asset',
63
- }),
64
- ]),
65
- time: expect.any(Number),
66
- fee: expect.any(String),
67
- })
68
- )
69
- })
70
- } catch {
71
- // Empty block
72
- }
73
- }
74
- )
75
-
76
- it.each([doraBDSNeoLegacy])('Should be able to get contract - %s', async (bdsNeoLegacy: BlockchainDataService) => {
77
- const hash = '0x998a0da7ec5f21c9a99ef5349f81af8af89f9644'
78
- const contract = await bdsNeoLegacy.getContract(hash)
79
- expect(contract).toEqual({
80
- hash: hash,
81
- name: 'Phantasma Stake',
82
- methods: [],
83
- })
84
- })
85
-
86
- it.each([doraBDSNeoLegacy])('Should be able to get token info - %s', async (bdsNeoLegacy: BlockchainDataService) => {
87
- const hash = '0x602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7'
88
- const token = await bdsNeoLegacy.getTokenInfo(hash)
89
- expect(token).toEqual({
90
- decimals: 8,
91
- hash: hash,
92
- name: 'GAS',
93
- symbol: 'GAS',
94
- })
95
- })
96
-
97
- it.each([doraBDSNeoLegacy])('Should be able to get balance - %s', async (bdsNeoLegacy: BlockchainDataService) => {
98
- const address = 'AeGgZTTWPzyVtNiQRcpngkV75Xip1hznmi'
99
- const balance = await bdsNeoLegacy.getBalance(address)
100
-
101
- balance.forEach(balance => {
102
- expect(balance).toEqual({
103
- amount: expect.any(String),
104
- token: {
105
- hash: expect.any(String),
106
- name: expect.any(String),
107
- symbol: expect.any(String),
108
- decimals: expect.any(Number),
109
- },
110
- })
111
- expect(balance.token.hash.startsWith('0x')).toBeFalsy()
112
- })
113
- })
114
-
115
- it.each([doraBDSNeoLegacy])(
116
- 'Should be able to get unclaimed - %s',
117
- async (doraBDSNeoLegacy: BlockchainDataService & BDSClaimable) => {
118
- const address = 'AeGgZTTWPzyVtNiQRcpngkV75Xip1hznmi'
119
- const unclaimed = await doraBDSNeoLegacy.getUnclaimed(address)
120
- expect(unclaimed).toEqual(expect.any(String))
121
- }
122
- )
123
- })