diff --git a/src/filecoin/FilecoinEngine.ts b/src/filecoin/FilecoinEngine.ts index 1d93aa0a2..39287c550 100644 --- a/src/filecoin/FilecoinEngine.ts +++ b/src/filecoin/FilecoinEngine.ts @@ -34,6 +34,7 @@ import { FilecoinWalletOtherData, SafeFilecoinWalletInfo } from './filecoinTypes' +import { Filfox, FilfoxMessage } from './Filfox' import { Filscan, FilscanMessage } from './Filscan' import { RpcExtra } from './RpcExtra' @@ -53,6 +54,7 @@ export class FilecoinEngine extends CurrencyEngine< // Backends: filRpc: RPC + filfoxApi: Filfox filscanApi: Filscan rpcExtra: RpcExtra @@ -70,6 +72,7 @@ export class FilecoinEngine extends CurrencyEngine< url: env.networkInfo.rpcNode.url, token: env.currencyInfo.currencyCode }) + this.filfoxApi = new Filfox(env.networkInfo.filfoxUrl, env.io.fetchCors) this.filscanApi = new Filscan(env.networkInfo.filscanUrl, env.io.fetchCors) this.rpcExtra = new RpcExtra(env.networkInfo.rpcNode.url, env.io.fetchCors) @@ -333,7 +336,8 @@ export class FilecoinEngine extends CurrencyEngine< } const scanners = [ - this.scanTransactionsFromFilscan(addressString, handleScan) + this.scanTransactionsFromFilscan(addressString, handleScan), + this.scanTransactionsFromFilfox(addressString, handleScan) ] await Promise.all(scanners) @@ -341,6 +345,49 @@ export class FilecoinEngine extends CurrencyEngine< handleScanProgress(1) } + async scanTransactionsFromFilfox( + address: string, + onScan: (event: { tx: EdgeTransaction; progress: number }) => void + ): Promise { + const messagesPerPage = 20 + let index = 0 + let messagesChecked = 0 + let messageCount = -1 + do { + const messagesResponse = await this.filfoxApi.getAccountMessages( + address, + index++, + messagesPerPage + ) + + // Only update the message count on the first query because mutating this + // in-between pagination may cause infinite loops. + messageCount = + messageCount === -1 ? messagesResponse.totalCount : messageCount + + const messages = messagesResponse.messages + 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 + const tx = this.filfoxMessageToEdgeTransaction(message) + + // Calculate the progress + const progress = + messageCount === 0 ? 1 : ++messagesChecked / messageCount + + onScan({ tx: tx, progress }) + } + } while (messagesChecked < messageCount) + } + async scanTransactionsFromFilscan( address: string, onScan: (event: { tx: EdgeTransaction; progress: number }) => void @@ -379,11 +426,43 @@ export class FilecoinEngine extends CurrencyEngine< const progress = messageCount === 0 ? 1 : ++messagesChecked / messageCount - onScan({ tx: tx, progress }) + onScan({ tx, progress }) } } while (messagesChecked < messageCount) } + filfoxMessageToEdgeTransaction = ( + message: FilfoxMessage + ): EdgeTransaction => { + 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.timestamp, + 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 + } + + return edgeTransaction + } + filscanMessageToEdgeTransaction(message: FilscanMessage): EdgeTransaction { const addressString = this.address.toString() let netNativeAmount = message.value diff --git a/src/filecoin/Filfox.ts b/src/filecoin/Filfox.ts new file mode 100644 index 000000000..4f4d79c7c --- /dev/null +++ b/src/filecoin/Filfox.ts @@ -0,0 +1,101 @@ +import { + asArray, + asEither, + asJSON, + asNumber, + asObject, + asString, + Cleaner +} from 'cleaners' +import { EdgeFetchFunction } from 'edge-core-js/types' + +// ----------------------------------------------------------------------------- +// Types +// ----------------------------------------------------------------------------- + +// +// Response Templates +// + +export type FilfoxError = ReturnType +export const asFilfoxError = asObject({ + statusCode: asNumber, + message: asString, + error: asString +}) + +export type FilfoxEnvelope = Result | FilfoxError +export const asFilfoxEnvelope = ( + asResult: Cleaner +): Cleaner> => asJSON(asEither(asFilfoxError, asResult)) + +// +// Nominal Types +// + +export type FilfoxMessage = ReturnType +export const asFilfoxMessage = asObject({ + cid: asString, + from: asString, + height: asNumber, + method: asString, + nonce: asNumber, + reciept: asObject({ + exitCode: asNumber + }), + timestamp: asNumber, + to: asString, + value: asString +}) + +// +// Messages +// + +export type FilfoxMessagesResult = ReturnType +export const asFilfoxMessagesResult = asObject({ + messages: asArray(asFilfoxMessage), + totalCount: asNumber +}) + +// ----------------------------------------------------------------------------- +// Implementation +// ----------------------------------------------------------------------------- + +export class Filfox { + baseUrl: string + fetch: EdgeFetchFunction + + constructor(baseUrl: string, fetchFn: EdgeFetchFunction) { + this.baseUrl = baseUrl + this.fetch = fetchFn + } + + async getAccountMessages( + address: string, + page: number, + pageSize: number = 20 + ): Promise { + const url = new URL(`${this.baseUrl}/address/${address}/messages`) + const searchParams = new URLSearchParams({ + page: page.toString(), + pageSize: pageSize.toString() + }) + url.search = searchParams.toString() + const response = await this.fetch(url.toString(), { + method: 'GET', + headers: { + 'content-type': 'application/json' + } + }) + const responseText = await response.text() + const responseBody = asFilfoxEnvelope(asFilfoxMessagesResult)(responseText) + + if ('error' in responseBody) + throw new Error( + `Error response code ${responseBody.statusCode}: ${responseBody.message} ${responseBody.error}` + ) + + return responseBody + } +} diff --git a/src/filecoin/filecoinInfo.ts b/src/filecoin/filecoinInfo.ts index 3e0345729..0a2d102e6 100644 --- a/src/filecoin/filecoinInfo.ts +++ b/src/filecoin/filecoinInfo.ts @@ -6,6 +6,7 @@ import type { FilecoinTools } from './FilecoinTools' import type { FilecoinNetworkInfo } from './filecoinTypes' const networkInfo: FilecoinNetworkInfo = { + filfoxUrl: 'https://filfox.info/api/v1', filscanUrl: 'https://api-v2.filscan.io/api/v1', hdPathCoinType: 461, rpcNode: { diff --git a/src/filecoin/filecoinTypes.ts b/src/filecoin/filecoinTypes.ts index 4358cb652..794f9c8ec 100644 --- a/src/filecoin/filecoinTypes.ts +++ b/src/filecoin/filecoinTypes.ts @@ -17,6 +17,7 @@ declare enum Network { Butterfly = 'butterfly' } export interface FilecoinNetworkInfo { + filfoxUrl: string filscanUrl: string hdPathCoinType: number rpcNode: {