@fioprotocol/fiosdk
Version:
The Foundation for Interwallet Operability (FIO) is a consortium of leading blockchain wallets, exchanges and payments providers that seeks to accelerate blockchain adoption by reducing the risk, complexity, and inconvenience of sending and receiving cryp
309 lines (278 loc) • 8.95 kB
JavaScript
import { FIOSDK } from '@fioprotocol/fiosdk'
import { bns } from 'biggystring'
import { asArray, asNumber, asObject, asOptional, asString } from 'cleaners'
const fetch = require('node-fetch')
export const asFioHistoryNodeAction = asObject({
account_action_seq: asNumber,
block_num: asNumber,
block_time: asString,
action_trace: asObject({
receiver: asString,
act: asObject({
account: asString,
name: asString,
data: asObject({
payee_public_key: asOptional(asString),
amount: asOptional(asNumber),
max_fee: asOptional(asNumber),
actor: asOptional(asString),
tpid: asOptional(asString),
quantity: asOptional(asString),
memo: asOptional(asString),
to: asOptional(asString),
from: asOptional(asString)
}),
hex_data: asString
}),
trx_id: asString,
block_num: asNumber,
block_time: asString,
producer_block_id: asString
})
})
export const asHistoryResponse = asObject({
actions: asArray(asFioHistoryNodeAction)
})
const HISTORY_NODE_OFFSET = 20
const fioPublicKey = 'FIO5CniznG2z6yVPc4as69si711R1HJMAAnC3Rxjd4kGri4Kp8D8P'
const fetchCors = async (uri, opts = {}) => {
return fetch(uri, opts)
}
async function getHighestTxHeight() {
// db query to get last highestTxHeight
}
async function saveHighestTxHeight(highestTxHeight) {
// db query to update highestTxHeight
}
async function findTransaction(trxId) {
// db query to get transaction
}
async function saveTransaction(trx) {
// db query to add/update transaction
}
function getDenomInfo(currencyCode) {
// { multiplier: string }
}
async function requestHistory(params: {
account_name: string,
pos: number,
offset: number
}) {
const result = await fetchCors(
'https://fio.greymass.com/v1/history/get_actions',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}
)
return result.json()
}
async function checkTransactions() {
const highestTxHeight = await getHighestTxHeight()
let newHighestTxHeight = highestTxHeight
let lastActionSeqNumber = 0
const fioSDK = new FIOSDK('', '', '', fetchCors, undefined, '')
// You can use any method you like to get actor value from public key
const actor = fioSDK.transactions.getActor(fioPublicKey)
try {
const lastActionObject = await requestHistory({
account_name: actor,
pos: -1,
offset: -1
})
asHistoryResponse(lastActionObject)
if (lastActionObject.actions.length) {
lastActionSeqNumber = lastActionObject.actions[0].account_action_seq
} else {
// if no transactions at all
return true
}
} catch (e) {
// handle error
return false
}
let pos = lastActionSeqNumber
let finish = false
while (!finish) {
if (pos < 0) {
break
}
let actionsObject
try {
actionsObject = await requestHistory({
account_name: actor,
pos,
offset: -HISTORY_NODE_OFFSET + 1
})
let actions = []
if (actionsObject.actions && actionsObject.actions.length > 0) {
actions = actionsObject.actions
} else {
break
}
for (let i = actions.length - 1; i > -1; i--) {
const action = actions[i]
asFioHistoryNodeAction(action)
const blockNum = processTransaction(action, actor)
if (blockNum > newHighestTxHeight) {
newHighestTxHeight = blockNum
} else if (
(blockNum === newHighestTxHeight && i === HISTORY_NODE_OFFSET - 1) ||
blockNum < highestTxHeight
) {
finish = true
break
}
}
if (!actions.length || actions.length < HISTORY_NODE_OFFSET) {
break
}
pos -= HISTORY_NODE_OFFSET
} catch (e) {
// handle error
return false
}
}
if (newHighestTxHeight > highestTxHeight) {
await saveHighestTxHeight(newHighestTxHeight)
}
return true
}
async function processTransaction(
action,
actor: string,
highestTxHeight: number
): number {
const {
act: { name: trxName, data }
} = action.action_trace
const currencyCode = 'FIO'
let nativeAmount
let actorSender
let fee = '0'
let otherParams: {
isTransferProcessed?: boolean,
isFeeProcessed?: boolean
} = {}
if (action.block_num <= highestTxHeight) {
return action.block_num
}
if (trxName !== 'trnsfiopubky' && trxName !== 'transfer') {
return action.block_num
}
// Transfer funds transaction
if (trxName === 'trnsfiopubky') {
// Get sent/received amount
nativeAmount = data.amount.toString()
// Set actor which sent tokens
actorSender = data.actor
// Check if transaction is sent or received
if (data.payee_public_key === fioPublicKey) {
// Check if sending to myself
if (actorSender === actor) {
nativeAmount = '0'
}
} else {
nativeAmount = `-${nativeAmount}`
}
const existingTrx = findTransaction(action.action_trace.trx_id)
// Check if fee transaction have already processed and update fee for existing transaction
if (existingTrx) {
// Copy existed params
otherParams = { ...existingTrx.otherParams }
// We should add fee only for sent transactions, not received. If amount is positive or 0 we skip this transactions because it already in the db.
if (bns.gte(nativeAmount, '0')) {
return action.block_num
}
// Check if `trnsfiopubky` transaction is processed
if (otherParams.isTransferProcessed) {
return action.block_num
}
// Check if fee (transfer) transaction is processed.
if (otherParams.isFeeProcessed) {
// Update amount and set fee to transaction. It would depend how you save transactions, in current example we save amount in transaction with fee calculated.
nativeAmount = bns.sub(nativeAmount, existingTrx.fee)
fee = existingTrx.fee
} else {
// Edge case
console.log(
'processTransaction error - existing spend transaction should have isTransferProcessed or isFeeProcessed set'
)
}
}
// Set flag informing that `trnsfiopubky` transaction is processed
otherParams.isTransferProcessed = true
const resTransaction = {
txid: action.action_trace.trx_id,
date: Date.parse(action.block_time) / 1000,
currencyCode,
blockHeight: action.block_num > 0 ? action.block_num : 0,
nativeAmount,
fee,
otherParams
}
await saveTransaction(resTransaction)
}
if (trxName === 'transfer') {
// For this tansaction type we have `quantity` in data object and value is such string - `2.000000000 FIO`
const [amount] = data.quantity.split(' ')
const exchangeAmount = amount.toString()
// Get multiplier for FIO to calculate native amount
const denom = getDenomInfo(this.currencyInfo, currencyCode)
if (!denom) {
this.log(`Received unsupported currencyCode: ${currencyCode}`)
return 0
}
// Calculate native amount
const fioAmount = bns.mul(exchangeAmount, denom.multiplier)
// Check if fee paid or received
if (data.to === actor) {
nativeAmount = `${fioAmount}`
} else {
// In this example we set native amount as (amount + fee)
nativeAmount = `-${fioAmount}`
fee = fioAmount
}
const existingTrx = findTransaction(action.action_trace.trx_id)
// Check if `transfer` (fee) transaction have already added and update fee for existing transaction
if (existingTrx) {
// Copy existed params
otherParams = { ...existingTrx.otherParams }
// We should add fee only for sent transactions, not received. If amount is positive or 0 we skip this transactions because it already in the db.
if (bns.gte(existingTrx.nativeAmount, '0')) {
return action.block_num
}
// Check if `transfer` have already processed
if (otherParams.isFeeProcessed) {
return action.block_num
}
// Check if `trnsfiopubky` have already processed
if (otherParams.isTransferProcessed) {
// Update native amount
nativeAmount = bns.sub(existingTrx.nativeAmount, fee)
} else {
console.log(
'processTransaction error - existing spend transaction should have isTransferProcessed or isFeeProcessed set'
)
}
}
// Set flag informing that `transfer` transaction is processed
otherParams.isFeeProcessed = true
const resTransaction = {
txid: action.action_trace.trx_id,
date: Date.parse(action.block_time) / 1000,
currencyCode,
blockHeight: action.block_num > 0 ? action.block_num : 0,
nativeAmount,
fee,
otherParams
}
await saveTransaction(resTransaction)
}
return action.block_num
}
checkTransactions()