Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Filecoin #607

Merged
merged 2 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"@hashgraph/sdk": "^1.1.9",
"@polkadot/api": "^10.9.1",
"@solana/web3.js": "^1.32.0",
"@zondax/izari-filecoin": "^1.2.0",
"algosdk": "^2.1.0",
"biggystring": "^4.1.3",
"bip39": "^3.0.2",
Expand Down
383 changes: 383 additions & 0 deletions src/filecoin/FilecoinEngine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,383 @@
import {
Address,
RPC,
Signature,
SignatureType,
Token,
Transaction,
Wallet
} from '@zondax/izari-filecoin'
import { add, lte, mul, sub } from 'biggystring'
import {
EdgeCurrencyEngine,
EdgeCurrencyEngineOptions,
EdgeEnginePrivateKeyOptions,
EdgeFreshAddress,
EdgeSpendInfo,
EdgeTransaction,
EdgeWalletInfo,
InsufficientFundsError,
JsonObject,
NoAmountSpecifiedError
} from 'edge-core-js/types'

import { CurrencyEngine } from '../common/CurrencyEngine'
import { PluginEnvironment } from '../common/innerPlugin'
import { FilecoinTools } from './FilecoinTools'
import {
asFilecoinPrivateKeys,
asFilecoinTxOtherParams,
asFilecoinWalletOtherData,
asSafeFilecoinWalletInfo,
FilecoinNetworkInfo,
FilecoinTxOtherParams,
FilecoinWalletOtherData,
SafeFilecoinWalletInfo
} from './filecoinTypes'
import { Filscan, FilscanMessage } from './Filscan'
import { RpcExtra } from './RpcExtra'

const CHECK_BALANCE_INTERVAL = 15000
const CHECK_BLOCKHEIGHT_INTERVAL = 30000
const CHECK_TRANSACTION_INTERVAL = 15000

export class FilecoinEngine extends CurrencyEngine<
FilecoinTools,
SafeFilecoinWalletInfo
> {
address: Address
availableAttoFil: string
networkInfo: FilecoinNetworkInfo
otherData!: FilecoinWalletOtherData
pluginId: string

// Backends:
filRpc: RPC
filscanApi: Filscan
rpcExtra: RpcExtra

constructor(
env: PluginEnvironment<FilecoinNetworkInfo>,
tools: FilecoinTools,
walletInfo: SafeFilecoinWalletInfo,
opts: EdgeCurrencyEngineOptions
) {
super(env, tools, walletInfo, opts)
const { networkInfo } = env
this.address = Address.fromString(walletInfo.keys.address)
this.availableAttoFil = '0'
this.filRpc = new RPC(env.networkInfo.rpcNode.networkName, {
url: env.networkInfo.rpcNode.url,
token: env.currencyInfo.currencyCode
})
this.filscanApi = new Filscan(env.networkInfo.filscanUrl, env.io.fetchCors)
this.rpcExtra = new RpcExtra(env.networkInfo.rpcNode.url, env.io.fetchCors)

this.networkInfo = networkInfo
this.pluginId = this.currencyInfo.pluginId
}

setOtherData(raw: any): void {
this.otherData = asFilecoinWalletOtherData(raw)
}

initData(): void {
// Initialize walletLocalData:
// ...

// Engine variables
this.availableAttoFil = '0'
}

initSubscriptions(): void {
this.addToLoop('checkBalance', CHECK_BALANCE_INTERVAL).catch(error =>
this.log(error)
)
this.addToLoop('checkBlockHeight', CHECK_BLOCKHEIGHT_INTERVAL).catch(
error => this.log(error)
)
this.addToLoop('checkTransactions', CHECK_TRANSACTION_INTERVAL).catch(
error => this.log(error)
)
}

onUpdateBlockHeight(networkBlockHeight: number): void {
if (this.walletLocalData.blockHeight !== networkBlockHeight) {
this.walletLocalData.blockHeight = networkBlockHeight
this.walletLocalDataDirty = true
this.currencyEngineCallbacks.onBlockHeightChanged(
this.walletLocalData.blockHeight
)
}
}

onUpdateTransactions(): void {
if (this.transactionsChangedArray.length > 0) {
this.currencyEngineCallbacks.onTransactionsChanged(
this.transactionsChangedArray
)
this.transactionsChangedArray = []
}
}

async startEngine(): Promise<void> {
this.initData()
this.initSubscriptions()
await super.startEngine()
}

async killEngine(): Promise<void> {
await super.killEngine()
}

async clearBlockchainCache(): Promise<void> {
await super.clearBlockchainCache()
}

async resyncBlockchain(): Promise<void> {
await super.killEngine()
await this.clearBlockchainCache()
await this.startEngine()
}

async getFreshAddress(): Promise<EdgeFreshAddress> {
const { address: publicAddress } = this.walletInfo.keys
return {
publicAddress
}
}

async getMaxSpendable(spendInfo: EdgeSpendInfo): Promise<string> {
const tx = await this.makeSpend(spendInfo)
const networkFee = tx.networkFee
const spendableBalance = sub(this.availableAttoFil, networkFee)

if (lte(spendableBalance, '0')) throw new InsufficientFundsError()

return spendableBalance
}

async makeSpend(edgeSpendInfoIn: EdgeSpendInfo): Promise<EdgeTransaction> {
const { edgeSpendInfo, currencyCode } = this.makeSpendCheck(edgeSpendInfoIn)
const spendTarget = edgeSpendInfo.spendTargets[0]
const { publicAddress, nativeAmount } = spendTarget

if (publicAddress == null)
throw new Error('Missing publicAddress in EdgeSpendInfo')
if (nativeAmount == null) throw new NoAmountSpecifiedError()

const toAddress = Address.fromString(publicAddress)

// Great new blank transaction:
const transaction = Transaction.getNew(
toAddress,
this.address, // from
Token.fromAtto(nativeAmount), // value
0 // method
)
// Add nonce and gas fields:
await transaction.prepareToSend(this.filRpc)

const txJson = transaction.toJSON()

const otherParams: FilecoinTxOtherParams = {
sigJson: undefined,
txJson
}

const networkFee = mul(txJson.GasLimit.toString(), txJson.GasPremium) // TODO: Include base fee and burn fee somehow?
const totalTxAmount = add(nativeAmount, networkFee)

const edgeTransaction: EdgeTransaction = {
txid: '',
date: 0,
currencyCode,
blockHeight: 0,
nativeAmount: `-${totalTxAmount}`,
isSend: nativeAmount.startsWith('-'),
networkFee,
ourReceiveAddresses: [],
otherParams,
signedTx: '',
walletId: this.walletId
}

return edgeTransaction
}

async signTx(
edgeTransaction: EdgeTransaction,
privateKeys: JsonObject
): Promise<EdgeTransaction> {
const otherParams = asFilecoinTxOtherParams(edgeTransaction.otherParams)
const transaction = Transaction.fromJSON(otherParams.txJson)

// Add signature JSON to otherParams:
const filecoinPrivateKeys = asFilecoinPrivateKeys(this.pluginId)(
privateKeys
)
const accountData = Wallet.deriveAccount(
filecoinPrivateKeys.mnemonic,
SignatureType.SECP256K1,
this.tools.derivationPath
)
const signature = await Wallet.signTransaction(accountData, transaction)
edgeTransaction.otherParams = {
...edgeTransaction.otherParams,
sigJson: signature.toJSON()
}

return edgeTransaction
}

async broadcastTx(
edgeTransaction: EdgeTransaction,
opts?: EdgeEnginePrivateKeyOptions
): Promise<EdgeTransaction> {
const otherParams = asFilecoinTxOtherParams(edgeTransaction.otherParams)

if (otherParams.sigJson == null)
throw new Error('Cannot broadcast unsigned transaction')

const signature: Signature = Signature.fromJSON(otherParams.sigJson)
const transaction: Transaction = Transaction.fromJSON(otherParams.txJson)

const response = await this.filRpc.broadcastTransaction(
transaction,
signature
)
if ('error' in response) throw new Error(response.error.message)

// Save CID as the txid
edgeTransaction.txid = response.result['/']

return edgeTransaction
}

getDisplayPrivateSeed(privateKeys: JsonObject): string {
const filecoinPrivateKeys = asFilecoinPrivateKeys(this.pluginId)(
privateKeys
)
return filecoinPrivateKeys.mnemonic
}

getDisplayPublicSeed(): string {
return this.walletInfo.keys.publicKey
}

async loadEngine(): Promise<void> {
await super.loadEngine()
this.engineOn = true
}

//
// Filecoin Engine Specific
//

async checkBalance(): Promise<void> {
const response = await this.filRpc.walletBalance(this.address)
if ('error' in response) throw new Error(response.error.message)

const { result: balance } = response
this.availableAttoFil = balance
this.updateBalance(this.currencyInfo.currencyCode, balance)
this.tokenCheckBalanceStatus[this.currencyInfo.currencyCode] = 1
this.updateOnAddressesChecked()
this.walletLocalDataDirty = true
}

async checkBlockHeight(): Promise<void> {
const response = await this.rpcExtra.getChainHead()
const blockHeight = response.result.Height

this.onUpdateBlockHeight(blockHeight)
}

async checkTransactions(): Promise<void> {
const addressString = this.address.toString()

const messagesPerPage = 20
let index = 0
let messagesChecked = 0
let messageCount = 0
do {
const messagesResponse = await this.filscanApi.getAccountMessages(
addressString,
index++,
messagesPerPage
)
const messages = messagesResponse.result.messages_by_account_id_list

for (const message of messages) {
const txid = message.cid
const idx = this.findTransaction(this.currencyInfo.currencyCode, txid)

if (idx >= 0) {
// Exit early because we reached transaction history from previous
// check
return
}

// Process message into a transaction
this.processMessage(message)
}

messageCount = messagesResponse.result.total_count
messagesChecked += messages.length
this.tokenCheckTransactionsStatus[this.currencyInfo.currencyCode] =
messagesChecked / messageCount
this.updateOnAddressesChecked()
} while (messagesChecked < messageCount)
}

processMessage(message: FilscanMessage): void {
const addressString = this.address.toString()
let netNativeAmount = message.value
const ourReceiveAddresses = []

const networkFee = '0' // TODO: calculate transaction fee from onchain gas fields
if (message.to !== addressString) {
// check if tx is a spend
netNativeAmount = `-${add(netNativeAmount, networkFee)}`
} else {
ourReceiveAddresses.push(addressString)
}

const edgeTransaction: EdgeTransaction = {
txid: message.cid,
date: message.block_time,
currencyCode: this.currencyInfo.currencyCode,
blockHeight: message.height,
nativeAmount: netNativeAmount,
isSend: netNativeAmount.startsWith('-'),
networkFee,
ourReceiveAddresses, // blank if you sent money otherwise array of addresses that are yours in this transaction
signedTx: '',
otherParams: {},
walletId: this.walletId
}
this.addTransaction(this.currencyInfo.currencyCode, edgeTransaction)
this.onUpdateTransactions()

// Progress the block-height if the message's height is greater than
// last poll for block-height.
if (this.walletLocalData.blockHeight < message.height) {
this.onUpdateBlockHeight(message.height)
}
}
}
export async function makeCurrencyEngine(
env: PluginEnvironment<FilecoinNetworkInfo>,
tools: FilecoinTools,
walletInfo: EdgeWalletInfo,
opts: EdgeCurrencyEngineOptions
): Promise<EdgeCurrencyEngine> {
const safeWalletInfo = asSafeFilecoinWalletInfo(walletInfo)

const engine = new FilecoinEngine(env, tools, safeWalletInfo, opts)

// Do any async initialization necessary for the engine
await engine.loadEngine()

return engine
}
Loading
Loading