mirror of
https://github.com/pezkuwichain/pezkuwi-subquery.git
synced 2026-04-25 15:08:01 +00:00
Initial commit: Pezkuwi SubQuery indexer
- pezkuwi.yaml: Relay chain staking indexer (rewards, slashes, pools, transfers, era info) - pezkuwi-assethub.yaml: Asset Hub indexer (NominationPools, asset transfers) - GraphQL schema for staking data entities - Handler mappings from Nova SubQuery base
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import "@polkadot/types-augment/lookup";
|
||||
import { SubstrateEvent } from "@subql/types";
|
||||
import { blockNumber } from "./common";
|
||||
import { AccountId } from "@polkadot/types/interfaces";
|
||||
import {
|
||||
PalletStakingRewardDestination,
|
||||
PalletNominationPoolsPoolMember,
|
||||
} from "@polkadot/types/lookup";
|
||||
import { Option } from "@polkadot/types";
|
||||
|
||||
// Due to memory consumption optimization `rewardDestinationByAddress` contains only one key
|
||||
let rewardDestinationByAddress: {
|
||||
[blockId: string]: { [address: string]: PalletStakingRewardDestination };
|
||||
} = {};
|
||||
let controllersByStash: { [blockId: string]: { [address: string]: string } } =
|
||||
{};
|
||||
|
||||
let parachainStakingRewardEra: { [blockId: string]: number } = {};
|
||||
|
||||
let poolMembers: {
|
||||
[blockId: number]: [string, PalletNominationPoolsPoolMember][];
|
||||
} = {};
|
||||
|
||||
export async function cachedRewardDestination(
|
||||
accountAddress: string,
|
||||
event: SubstrateEvent,
|
||||
): Promise<PalletStakingRewardDestination> {
|
||||
const blockId = blockNumber(event);
|
||||
let cachedBlock = rewardDestinationByAddress[blockId];
|
||||
|
||||
if (cachedBlock !== undefined) {
|
||||
return cachedBlock[accountAddress];
|
||||
} else {
|
||||
rewardDestinationByAddress = {};
|
||||
|
||||
let method = event.event.method;
|
||||
let section = event.event.section;
|
||||
|
||||
const allEventsInBlock = event.block.events.filter((blockEvent) => {
|
||||
return (
|
||||
blockEvent.event.method == method && blockEvent.event.section == section
|
||||
);
|
||||
});
|
||||
|
||||
let destinationByAddress: {
|
||||
[address: string]: PalletStakingRewardDestination;
|
||||
} = {};
|
||||
|
||||
let {
|
||||
event: { data: innerData },
|
||||
} = event;
|
||||
|
||||
if (innerData.length == 3) {
|
||||
allEventsInBlock.forEach((event) => {
|
||||
let {
|
||||
event: {
|
||||
data: [accountId, destination, _],
|
||||
},
|
||||
} = event;
|
||||
let accountAddress = accountId.toString();
|
||||
destinationByAddress[accountAddress] =
|
||||
destination as PalletStakingRewardDestination;
|
||||
});
|
||||
} else {
|
||||
const allAccountsInBlock = allEventsInBlock.map((event) => {
|
||||
let {
|
||||
event: {
|
||||
data: [accountId],
|
||||
},
|
||||
} = event;
|
||||
return accountId;
|
||||
});
|
||||
|
||||
// looks like accountAddress not related to events so just try to query payee directly
|
||||
if (allAccountsInBlock.length === 0) {
|
||||
rewardDestinationByAddress[blockId] = {};
|
||||
return (await api.query.staking.payee(
|
||||
accountAddress,
|
||||
)) as unknown as PalletStakingRewardDestination;
|
||||
}
|
||||
|
||||
// TODO: Commented code doesn't work now, may be fixed later
|
||||
// const payees = await api.query.staking.payee.multi(allAccountsInBlock);
|
||||
const payees = await api.queryMulti(
|
||||
allAccountsInBlock.map((account) => [api.query.staking.payee, account]),
|
||||
);
|
||||
|
||||
const rewardDestinations = payees.map((payee) => {
|
||||
return payee as PalletStakingRewardDestination;
|
||||
});
|
||||
|
||||
// something went wrong, so just query for single accountAddress
|
||||
if (rewardDestinations.length !== allAccountsInBlock.length) {
|
||||
const payee = (await api.query.staking.payee(
|
||||
accountAddress,
|
||||
)) as unknown as PalletStakingRewardDestination;
|
||||
destinationByAddress[accountAddress] = payee;
|
||||
rewardDestinationByAddress[blockId] = destinationByAddress;
|
||||
return payee;
|
||||
}
|
||||
allAccountsInBlock.forEach((account, index) => {
|
||||
let accountAddress = account.toString();
|
||||
let rewardDestination = rewardDestinations[index];
|
||||
destinationByAddress[accountAddress] = rewardDestination;
|
||||
});
|
||||
}
|
||||
|
||||
rewardDestinationByAddress[blockId] = destinationByAddress;
|
||||
return destinationByAddress[accountAddress];
|
||||
}
|
||||
}
|
||||
|
||||
export async function cachedController(
|
||||
accountAddress: string,
|
||||
event: SubstrateEvent,
|
||||
): Promise<string> {
|
||||
const blockId = blockNumber(event);
|
||||
let cachedBlock = controllersByStash[blockId];
|
||||
|
||||
if (cachedBlock !== undefined) {
|
||||
return cachedBlock[accountAddress];
|
||||
} else {
|
||||
controllersByStash = {};
|
||||
|
||||
let method = event.event.method;
|
||||
let section = event.event.section;
|
||||
|
||||
const allAccountsInBlock = event.block.events
|
||||
.filter((blockEvent) => {
|
||||
return (
|
||||
blockEvent.event.method == method &&
|
||||
blockEvent.event.section == section
|
||||
);
|
||||
})
|
||||
.map((event) => {
|
||||
let {
|
||||
event: {
|
||||
data: [accountId],
|
||||
},
|
||||
} = event;
|
||||
return accountId;
|
||||
});
|
||||
|
||||
var controllerNeedAccounts: AccountId[] = [];
|
||||
|
||||
for (let accountId of allAccountsInBlock) {
|
||||
const rewardDestination = await cachedRewardDestination(
|
||||
accountId.toString(),
|
||||
event,
|
||||
);
|
||||
|
||||
if (rewardDestination.isController) {
|
||||
controllerNeedAccounts.push(accountId as AccountId);
|
||||
}
|
||||
}
|
||||
|
||||
// looks like accountAddress not related to events so just try to query controller directly
|
||||
if (controllerNeedAccounts.length === 0) {
|
||||
controllersByStash[blockId] = {};
|
||||
let accountId = await api.query.staking.bonded(accountAddress);
|
||||
return accountId.toString();
|
||||
}
|
||||
|
||||
// TODO: Commented code doesn't work now, may be fixed later
|
||||
// const bonded = await api.query.staking.bonded.multi(controllerNeedAccounts);
|
||||
const bonded = await api.queryMulti(
|
||||
controllerNeedAccounts.map((account) => [
|
||||
api.query.staking.bonded,
|
||||
account,
|
||||
]),
|
||||
);
|
||||
const controllers = bonded.map((bonded) => {
|
||||
return bonded.toString();
|
||||
});
|
||||
|
||||
let bondedByAddress: { [address: string]: string } = {};
|
||||
|
||||
// something went wrong, so just query for single accountAddress
|
||||
if (controllers.length !== controllerNeedAccounts.length) {
|
||||
const controller = await api.query.staking.bonded(accountAddress);
|
||||
let controllerAddress = controller.toString();
|
||||
bondedByAddress[accountAddress] = controllerAddress;
|
||||
controllersByStash[blockId] = bondedByAddress;
|
||||
return controllerAddress;
|
||||
}
|
||||
controllerNeedAccounts.forEach((account, index) => {
|
||||
let accountAddress = account.toString();
|
||||
bondedByAddress[accountAddress] = controllers[index];
|
||||
});
|
||||
controllersByStash[blockId] = bondedByAddress;
|
||||
return bondedByAddress[accountAddress];
|
||||
}
|
||||
}
|
||||
|
||||
export async function cachedStakingRewardEraIndex(
|
||||
event: SubstrateEvent,
|
||||
): Promise<number> {
|
||||
const blockId = blockNumber(event);
|
||||
let cachedEra = parachainStakingRewardEra[blockId];
|
||||
|
||||
if (cachedEra !== undefined) {
|
||||
return cachedEra;
|
||||
} else {
|
||||
const era = await api.query.parachainStaking.round();
|
||||
|
||||
const paymentDelay =
|
||||
api.consts.parachainStaking.rewardPaymentDelay.toHuman();
|
||||
// HACK: used to get data from object
|
||||
const eraIndex =
|
||||
(era.toJSON() as { current: any }).current - Number(paymentDelay);
|
||||
|
||||
parachainStakingRewardEra = {};
|
||||
parachainStakingRewardEra[blockId] = eraIndex;
|
||||
return eraIndex;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPoolMembers(
|
||||
blockId: number,
|
||||
): Promise<[string, PalletNominationPoolsPoolMember][]> {
|
||||
const cachedMembers = poolMembers[blockId];
|
||||
if (cachedMembers != undefined) {
|
||||
return cachedMembers;
|
||||
}
|
||||
|
||||
const members: [string, PalletNominationPoolsPoolMember][] = (
|
||||
await api.query.nominationPools.poolMembers.entries()
|
||||
)
|
||||
.filter(
|
||||
([_, member]) =>
|
||||
(member as Option<PalletNominationPoolsPoolMember>).isSome,
|
||||
)
|
||||
.map(([accountId, member]) => [
|
||||
accountId.args[0].toString(),
|
||||
(member as Option<PalletNominationPoolsPoolMember>).unwrap(),
|
||||
]);
|
||||
poolMembers = {};
|
||||
poolMembers[blockId] = members;
|
||||
return members;
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
import { SubstrateExtrinsic } from "@subql/types";
|
||||
import { AssetTransfer, HistoryElement, Transfer, Swap } from "../types";
|
||||
import {
|
||||
getAssetIdFromMultilocation,
|
||||
getEventData,
|
||||
callFromProxy,
|
||||
callsFromBatch,
|
||||
calculateFeeAsString,
|
||||
extrinsicIdFromBlockAndIdx,
|
||||
eventRecordToSubstrateEvent,
|
||||
isBatch,
|
||||
isProxy,
|
||||
timestamp,
|
||||
isNativeTransfer,
|
||||
isAssetTransfer,
|
||||
isOrmlTransfer,
|
||||
isSwapExactTokensForTokens,
|
||||
isSwapTokensForExactTokens,
|
||||
isNativeTransferAll,
|
||||
isOrmlTransferAll,
|
||||
isEvmTransaction,
|
||||
isEvmExecutedEvent,
|
||||
isAssetTxFeePaidEvent,
|
||||
isEquilibriumTransfer,
|
||||
isHydraOmnipoolBuy,
|
||||
isHydraOmnipoolSell,
|
||||
isHydraRouterSell,
|
||||
isHydraRouterBuy,
|
||||
convertOrmlCurrencyIdToString,
|
||||
} from "./common";
|
||||
import { CallBase } from "@polkadot/types/types/calls";
|
||||
import { AnyTuple } from "@polkadot/types/types/codec";
|
||||
import { u64 } from "@polkadot/types";
|
||||
import { ethereumEncode } from "@polkadot/util-crypto";
|
||||
import { u128, u32 } from "@polkadot/types-codec";
|
||||
import { convertHydraDxTokenIdToString, findHydraDxFeeTyped } from "./swaps";
|
||||
import { Codec } from "@polkadot/types/types";
|
||||
|
||||
type TransferData = {
|
||||
isTransferAll: boolean;
|
||||
transfer: Transfer | AssetTransfer | Swap;
|
||||
};
|
||||
|
||||
type TransferCallback = (
|
||||
isTransferAll: boolean,
|
||||
address: string,
|
||||
amount: any,
|
||||
assetId?: string
|
||||
) => Array<{ isTransferAll: boolean; transfer: Transfer }>;
|
||||
|
||||
type AssetHubSwapCallback = (
|
||||
path: any,
|
||||
amountId: Codec,
|
||||
amountOut: Codec,
|
||||
receiver: Codec
|
||||
) => Array<{ isTransferAll: boolean; transfer: Swap }>;
|
||||
|
||||
type HydraDxSwapCallback = (
|
||||
assetIn: Codec,
|
||||
assetOut: Codec,
|
||||
amountIn: Codec,
|
||||
amountOut: Codec
|
||||
) => { isTransferAll: boolean; transfer: Swap };
|
||||
|
||||
export async function handleHistoryElement(
|
||||
extrinsic: SubstrateExtrinsic
|
||||
): Promise<void> {
|
||||
const { isSigned } = extrinsic.extrinsic;
|
||||
|
||||
if (isSigned) {
|
||||
let failedTransfers = findFailedTransferCalls(extrinsic);
|
||||
if (failedTransfers != null) {
|
||||
await saveFailedTransfers(failedTransfers, extrinsic);
|
||||
} else {
|
||||
await saveExtrinsic(extrinsic);
|
||||
}
|
||||
} else if (
|
||||
isEvmTransaction(extrinsic.extrinsic.method) &&
|
||||
extrinsic.success
|
||||
) {
|
||||
await saveEvmExtrinsic(extrinsic);
|
||||
}
|
||||
}
|
||||
|
||||
function createHistoryElement(
|
||||
extrinsic: SubstrateExtrinsic,
|
||||
address: string,
|
||||
suffix: string = "",
|
||||
hash?: string
|
||||
) {
|
||||
let extrinsicHash = hash || extrinsic.extrinsic.hash.toString();
|
||||
let blockNumber = extrinsic.block.block.header.number.toNumber();
|
||||
let extrinsicIdx = extrinsic.idx;
|
||||
let extrinsicId = extrinsicIdFromBlockAndIdx(blockNumber, extrinsicIdx);
|
||||
let blockTimestamp = timestamp(extrinsic.block);
|
||||
|
||||
const historyElement = HistoryElement.create({
|
||||
id: `${extrinsicId}${suffix}`,
|
||||
blockNumber,
|
||||
timestamp: blockTimestamp,
|
||||
address,
|
||||
});
|
||||
historyElement.extrinsicHash = extrinsicHash;
|
||||
historyElement.extrinsicIdx = extrinsicIdx;
|
||||
historyElement.timestamp = blockTimestamp;
|
||||
|
||||
return historyElement;
|
||||
}
|
||||
|
||||
function addTransferToHistoryElement(
|
||||
element: HistoryElement,
|
||||
transfer: Transfer | AssetTransfer | Swap
|
||||
) {
|
||||
if ("assetIdIn" in transfer) {
|
||||
element.swap = transfer;
|
||||
} else if ("assetId" in transfer) {
|
||||
element.assetTransfer = transfer;
|
||||
} else {
|
||||
element.transfer = transfer;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFailedTransfers(
|
||||
transfers: Array<TransferData>,
|
||||
extrinsic: SubstrateExtrinsic
|
||||
): Promise<void> {
|
||||
for (const { isTransferAll, transfer } of transfers) {
|
||||
const isSwap = "assetIdIn" in transfer;
|
||||
const from = isSwap ? transfer.sender : transfer.from;
|
||||
const to = isSwap ? transfer.receiver : transfer.to;
|
||||
const elementFrom = createHistoryElement(extrinsic, from, `-from`);
|
||||
addTransferToHistoryElement(elementFrom, transfer);
|
||||
|
||||
// FIXME: Try to find more appropriate way to handle failed transferAll events
|
||||
if ((!isTransferAll && !isSwap) || from.toString() != to.toString()) {
|
||||
const elementTo = createHistoryElement(extrinsic, to, `-to`);
|
||||
addTransferToHistoryElement(elementTo, transfer);
|
||||
|
||||
await elementTo.save();
|
||||
}
|
||||
|
||||
await elementFrom.save();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveExtrinsic(extrinsic: SubstrateExtrinsic): Promise<void> {
|
||||
const element = createHistoryElement(
|
||||
extrinsic,
|
||||
extrinsic.extrinsic.signer.toString(),
|
||||
"-extrinsic"
|
||||
);
|
||||
|
||||
element.extrinsic = {
|
||||
hash: extrinsic.extrinsic.hash.toString(),
|
||||
module: extrinsic.extrinsic.method.section,
|
||||
call: extrinsic.extrinsic.method.method,
|
||||
success: extrinsic.success,
|
||||
fee: calculateFeeAsString(extrinsic),
|
||||
};
|
||||
await element.save();
|
||||
}
|
||||
|
||||
async function saveEvmExtrinsic(extrinsic: SubstrateExtrinsic): Promise<void> {
|
||||
const executedEvent = extrinsic.events.find(isEvmExecutedEvent);
|
||||
if (!executedEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addressFrom = ethereumEncode(executedEvent.event.data?.[0]?.toString());
|
||||
const hash = executedEvent.event.data?.[2]?.toString();
|
||||
const success = !!(executedEvent.event.data?.[3].toJSON() as any).succeed;
|
||||
|
||||
const element = createHistoryElement(
|
||||
extrinsic,
|
||||
addressFrom,
|
||||
"-extrinsic",
|
||||
hash
|
||||
);
|
||||
|
||||
element.extrinsic = {
|
||||
hash,
|
||||
module: extrinsic.extrinsic.method.section,
|
||||
call: extrinsic.extrinsic.method.method,
|
||||
success,
|
||||
fee: calculateFeeAsString(extrinsic, addressFrom),
|
||||
};
|
||||
|
||||
await element.save();
|
||||
}
|
||||
|
||||
/// Success Transfer emits Transfer event that is handled at Transfers.ts handleTransfer()
|
||||
function findFailedTransferCalls(
|
||||
extrinsic: SubstrateExtrinsic
|
||||
): Array<TransferData> | null {
|
||||
if (extrinsic.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let sender = extrinsic.extrinsic.signer;
|
||||
const transferCallback: TransferCallback = (
|
||||
isTransferAll,
|
||||
address,
|
||||
amount,
|
||||
assetId?
|
||||
) => {
|
||||
const transfer: Transfer = {
|
||||
amount: amount.toString(),
|
||||
from: sender.toString(),
|
||||
to: address,
|
||||
fee: calculateFeeAsString(extrinsic),
|
||||
eventIdx: -1,
|
||||
success: false,
|
||||
};
|
||||
|
||||
if (assetId) {
|
||||
(transfer as AssetTransfer).assetId = assetId;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
isTransferAll,
|
||||
transfer,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const assetHubSwapCallback: AssetHubSwapCallback = (
|
||||
path,
|
||||
amountIn,
|
||||
amountOut,
|
||||
receiver
|
||||
) => {
|
||||
let assetIdFee = "native";
|
||||
let fee = calculateFeeAsString(extrinsic);
|
||||
let foundAssetTxFeePaid = extrinsic.block.events.find((e) =>
|
||||
isAssetTxFeePaidEvent(eventRecordToSubstrateEvent(e))
|
||||
);
|
||||
if (foundAssetTxFeePaid !== undefined) {
|
||||
const [who, actual_fee, tip, rawAssetIdFee] = getEventData(
|
||||
eventRecordToSubstrateEvent(foundAssetTxFeePaid)
|
||||
);
|
||||
if ("interior" in rawAssetIdFee) {
|
||||
assetIdFee = getAssetIdFromMultilocation(rawAssetIdFee);
|
||||
fee = actual_fee.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const assetIdIn = getAssetIdFromMultilocation(path[0], true);
|
||||
const assetIdOut = getAssetIdFromMultilocation(
|
||||
path[path["length"] - 1],
|
||||
true
|
||||
);
|
||||
|
||||
if (assetIdIn === undefined || assetIdOut === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const swap: Swap = {
|
||||
assetIdIn: assetIdIn,
|
||||
amountIn: amountIn.toString(),
|
||||
assetIdOut: assetIdOut,
|
||||
amountOut: amountOut.toString(),
|
||||
sender: sender.toString(),
|
||||
receiver: receiver.toString(),
|
||||
assetIdFee: assetIdFee,
|
||||
fee: fee,
|
||||
eventIdx: -1,
|
||||
success: false,
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
isTransferAll: false,
|
||||
transfer: swap,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const hydraDxSwapCallback: HydraDxSwapCallback = (
|
||||
assetIn: Codec,
|
||||
assetOut: Codec,
|
||||
amountIn: Codec,
|
||||
amountOut: Codec
|
||||
) => {
|
||||
let fee = findHydraDxFeeTyped(extrinsic.events);
|
||||
|
||||
const assetIdIn = convertHydraDxTokenIdToString(assetIn);
|
||||
const assetIdOut = convertHydraDxTokenIdToString(assetOut);
|
||||
|
||||
const swap: Swap = {
|
||||
assetIdIn: assetIdIn,
|
||||
amountIn: amountIn.toString(),
|
||||
assetIdOut: assetIdOut,
|
||||
amountOut: amountOut.toString(),
|
||||
sender: sender.toString(),
|
||||
receiver: sender.toString(),
|
||||
assetIdFee: fee.tokenId,
|
||||
fee: fee.amount,
|
||||
eventIdx: -1,
|
||||
success: false,
|
||||
};
|
||||
|
||||
return {
|
||||
isTransferAll: false,
|
||||
transfer: swap,
|
||||
};
|
||||
};
|
||||
|
||||
let transferCalls = determineTransferCallsArgs(
|
||||
extrinsic.extrinsic.method,
|
||||
transferCallback,
|
||||
assetHubSwapCallback,
|
||||
hydraDxSwapCallback
|
||||
);
|
||||
if (transferCalls.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transferCalls;
|
||||
}
|
||||
|
||||
function determineTransferCallsArgs(
|
||||
causeCall: CallBase<AnyTuple>,
|
||||
transferCallback: TransferCallback,
|
||||
assetHubSwapCallback: AssetHubSwapCallback,
|
||||
hydraDxSwapCallback: HydraDxSwapCallback
|
||||
): Array<TransferData> {
|
||||
if (isNativeTransfer(causeCall)) {
|
||||
return transferCallback(false, ...extractArgsFromTransfer(causeCall));
|
||||
} else if (isAssetTransfer(causeCall)) {
|
||||
return transferCallback(false, ...extractArgsFromAssetTransfer(causeCall));
|
||||
} else if (isOrmlTransfer(causeCall)) {
|
||||
return transferCallback(false, ...extractArgsFromOrmlTransfer(causeCall));
|
||||
} else if (isEquilibriumTransfer(causeCall)) {
|
||||
return transferCallback(
|
||||
false,
|
||||
...extractArgsFromEquilibriumTransfer(causeCall)
|
||||
);
|
||||
} else if (isNativeTransferAll(causeCall)) {
|
||||
return transferCallback(true, ...extractArgsFromTransferAll(causeCall));
|
||||
} else if (isOrmlTransferAll(causeCall)) {
|
||||
return transferCallback(true, ...extractArgsFromOrmlTransferAll(causeCall));
|
||||
} else if (isSwapExactTokensForTokens(causeCall)) {
|
||||
return assetHubSwapCallback(
|
||||
...extractArgsFromSwapExactTokensForTokens(causeCall)
|
||||
);
|
||||
} else if (isSwapTokensForExactTokens(causeCall)) {
|
||||
return assetHubSwapCallback(
|
||||
...extractArgsFromSwapTokensForExactTokens(causeCall)
|
||||
);
|
||||
} else if (isHydraOmnipoolBuy(causeCall)) {
|
||||
return [hydraDxSwapCallback(...extractArgsFromHydraOmnipoolBuy(causeCall))];
|
||||
} else if (isHydraOmnipoolSell(causeCall)) {
|
||||
return [
|
||||
hydraDxSwapCallback(...extractArgsFromHydraOmnipoolSell(causeCall)),
|
||||
];
|
||||
} else if (isHydraRouterBuy(causeCall)) {
|
||||
return [hydraDxSwapCallback(...extractArgsFromHydraRouterBuy(causeCall))];
|
||||
} else if (isHydraRouterSell(causeCall)) {
|
||||
return [hydraDxSwapCallback(...extractArgsFromHydraRouterSell(causeCall))];
|
||||
} else if (isBatch(causeCall)) {
|
||||
return callsFromBatch(causeCall)
|
||||
.map((call) => {
|
||||
return determineTransferCallsArgs(
|
||||
call,
|
||||
transferCallback,
|
||||
assetHubSwapCallback,
|
||||
hydraDxSwapCallback
|
||||
).map((value, index, array) => {
|
||||
return value;
|
||||
});
|
||||
})
|
||||
.flat();
|
||||
} else if (isProxy(causeCall)) {
|
||||
let proxyCall = callFromProxy(causeCall);
|
||||
return determineTransferCallsArgs(
|
||||
proxyCall,
|
||||
transferCallback,
|
||||
assetHubSwapCallback,
|
||||
hydraDxSwapCallback
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function extractArgsFromTransfer(call: CallBase<AnyTuple>): [string, bigint] {
|
||||
const [destinationAddress, amount] = call.args;
|
||||
|
||||
return [destinationAddress.toString(), (amount as u64).toBigInt()];
|
||||
}
|
||||
|
||||
function extractArgsFromAssetTransfer(
|
||||
call: CallBase<AnyTuple>
|
||||
): [string, bigint, string] {
|
||||
const [assetId, destinationAddress, amount] = call.args;
|
||||
|
||||
return [
|
||||
destinationAddress.toString(),
|
||||
(amount as u64).toBigInt(),
|
||||
assetId.toString(),
|
||||
];
|
||||
}
|
||||
|
||||
function extractArgsFromOrmlTransfer(
|
||||
call: CallBase<AnyTuple>
|
||||
): [string, bigint, string] {
|
||||
const [destinationAddress, currencyId, amount] = call.args;
|
||||
|
||||
return [
|
||||
destinationAddress.toString(),
|
||||
(amount as u64).toBigInt(),
|
||||
currencyId.toHex().toString(),
|
||||
];
|
||||
}
|
||||
|
||||
function extractArgsFromEquilibriumTransfer(
|
||||
call: CallBase<AnyTuple>
|
||||
): [string, bigint, string] {
|
||||
const [assetId, destinationAddress, amount] = call.args;
|
||||
|
||||
return [
|
||||
destinationAddress.toString(),
|
||||
(amount as u64).toBigInt(),
|
||||
assetId.toString(),
|
||||
];
|
||||
}
|
||||
|
||||
function extractArgsFromTransferAll(
|
||||
call: CallBase<AnyTuple>
|
||||
): [string, bigint] {
|
||||
const [destinationAddress] = call.args;
|
||||
|
||||
return [destinationAddress.toString(), BigInt(0)];
|
||||
}
|
||||
|
||||
function extractArgsFromOrmlTransferAll(
|
||||
call: CallBase<AnyTuple>
|
||||
): [string, bigint, string] {
|
||||
const [destinationAddress, currencyId] = call.args;
|
||||
|
||||
return [
|
||||
destinationAddress.toString(),
|
||||
BigInt(0),
|
||||
convertOrmlCurrencyIdToString(currencyId),
|
||||
];
|
||||
}
|
||||
|
||||
function extractArgsFromSwapExactTokensForTokens(
|
||||
call: CallBase<AnyTuple>
|
||||
): [any, Codec, Codec, Codec] {
|
||||
const [path, amountIn, amountOut, receiver, _] = call.args;
|
||||
|
||||
return [path, amountIn, amountOut, receiver];
|
||||
}
|
||||
|
||||
function extractArgsFromSwapTokensForExactTokens(
|
||||
call: CallBase<AnyTuple>
|
||||
): [any, Codec, Codec, Codec] {
|
||||
const [path, amountOut, amountIn, receiver, _] = call.args;
|
||||
|
||||
return [path, amountIn, amountOut, receiver];
|
||||
}
|
||||
|
||||
function extractArgsFromHydraRouterSell(
|
||||
call: CallBase<AnyTuple>
|
||||
): [Codec, Codec, Codec, Codec] {
|
||||
const [assetIn, assetOut, amountIn, minAmountOut, _] = call.args;
|
||||
|
||||
return [assetIn, assetOut, amountIn, minAmountOut];
|
||||
}
|
||||
|
||||
function extractArgsFromHydraRouterBuy(
|
||||
call: CallBase<AnyTuple>
|
||||
): [Codec, Codec, Codec, Codec] {
|
||||
const [assetIn, assetOut, amountOut, maxAmountIn, _] = call.args;
|
||||
|
||||
return [assetIn, assetOut, maxAmountIn, amountOut];
|
||||
}
|
||||
|
||||
function extractArgsFromHydraOmnipoolSell(
|
||||
call: CallBase<AnyTuple>
|
||||
): [Codec, Codec, Codec, Codec] {
|
||||
const [assetIn, assetOut, amount, minBuyAmount, _] = call.args;
|
||||
|
||||
return [
|
||||
assetIn,
|
||||
assetOut,
|
||||
amount, // amountIn
|
||||
minBuyAmount, // amountOut
|
||||
];
|
||||
}
|
||||
|
||||
function extractArgsFromHydraOmnipoolBuy(
|
||||
call: CallBase<AnyTuple>
|
||||
): [Codec, Codec, Codec, Codec] {
|
||||
const [assetOut, assetIn, amount, maxSellAmount, _] = call.args;
|
||||
|
||||
return [
|
||||
assetIn,
|
||||
assetOut,
|
||||
maxSellAmount, // amountIn
|
||||
amount, // amountOut
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { SubstrateEvent } from "@subql/types";
|
||||
import { eventId } from "./common";
|
||||
import { EraValidatorInfo } from "../types/models/EraValidatorInfo";
|
||||
import { IndividualExposure } from "../types";
|
||||
import {
|
||||
SpStakingPagedExposureMetadata,
|
||||
SpStakingExposurePage,
|
||||
} from "@polkadot/types/lookup";
|
||||
import { Option } from "@polkadot/types";
|
||||
import { INumber } from "@polkadot/types-codec/types/interfaces";
|
||||
import { Exposure } from "@polkadot/types/interfaces";
|
||||
|
||||
export async function handleStakersElected(
|
||||
event: SubstrateEvent,
|
||||
): Promise<void> {
|
||||
await handleNewEra(event);
|
||||
}
|
||||
|
||||
export async function handleNewEra(event: SubstrateEvent): Promise<void> {
|
||||
const currentEra = ((await api.query.staking.currentEra()) as Option<INumber>)
|
||||
.unwrap()
|
||||
.toNumber();
|
||||
|
||||
if (api.query.staking.erasStakersOverview) {
|
||||
await processEraStakersPaged(event, currentEra);
|
||||
} else {
|
||||
await processEraStakersClipped(event, currentEra);
|
||||
}
|
||||
}
|
||||
|
||||
async function processEraStakersClipped(
|
||||
event: SubstrateEvent,
|
||||
currentEra: number,
|
||||
): Promise<void> {
|
||||
const exposures =
|
||||
await api.query.staking.erasStakersClipped.entries(currentEra);
|
||||
|
||||
for (const [key, exposure] of exposures) {
|
||||
const [, validatorId] = key.args;
|
||||
let validatorIdString = validatorId.toString();
|
||||
const exp = exposure as unknown as Exposure;
|
||||
const eraValidatorInfo = new EraValidatorInfo(
|
||||
eventId(event) + validatorIdString,
|
||||
validatorIdString,
|
||||
currentEra,
|
||||
exp.total.toBigInt(),
|
||||
exp.own.toBigInt(),
|
||||
exp.others.map((other) => {
|
||||
return {
|
||||
who: other.who.toString(),
|
||||
value: other.value.toString(),
|
||||
} as IndividualExposure;
|
||||
}),
|
||||
);
|
||||
await eraValidatorInfo.save();
|
||||
}
|
||||
}
|
||||
|
||||
async function processEraStakersPaged(
|
||||
event: SubstrateEvent,
|
||||
currentEra: number,
|
||||
): Promise<void> {
|
||||
const overview =
|
||||
await api.query.staking.erasStakersOverview.entries(currentEra);
|
||||
const pages = await api.query.staking.erasStakersPaged.entries(currentEra);
|
||||
|
||||
interface AccumulatorType {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const othersCounted = pages.reduce(
|
||||
(accumulator: AccumulatorType, [key, exp]) => {
|
||||
const exposure = (
|
||||
exp as unknown as Option<SpStakingExposurePage>
|
||||
).unwrap();
|
||||
const [, validatorId, pageId] = key.args;
|
||||
const pageNumber = (pageId as INumber).toNumber();
|
||||
const validatorIdString = validatorId.toString();
|
||||
|
||||
const others = exposure.others.map(({ who, value }) => {
|
||||
return {
|
||||
who: who.toString(),
|
||||
value: value.toString(),
|
||||
} as IndividualExposure;
|
||||
});
|
||||
|
||||
(accumulator[validatorIdString] = accumulator[validatorIdString] || {})[
|
||||
pageNumber
|
||||
] = others;
|
||||
return accumulator;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
for (const [key, exp] of overview) {
|
||||
const exposure = (
|
||||
exp as unknown as Option<SpStakingPagedExposureMetadata>
|
||||
).unwrap();
|
||||
const [, validatorId] = key.args;
|
||||
let validatorIdString = validatorId.toString();
|
||||
|
||||
let others = [];
|
||||
for (let i = 0; i < exposure.pageCount.toNumber(); ++i) {
|
||||
others.push(...othersCounted[validatorIdString][i]);
|
||||
}
|
||||
|
||||
const eraValidatorInfo = new EraValidatorInfo(
|
||||
eventId(event) + validatorIdString,
|
||||
validatorIdString,
|
||||
currentEra,
|
||||
exposure.total.toBigInt(),
|
||||
exposure.own.toBigInt(),
|
||||
others,
|
||||
);
|
||||
await eraValidatorInfo.save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
import {
|
||||
AccountPoolReward,
|
||||
AccumulatedReward,
|
||||
AccumulatedPoolReward,
|
||||
HistoryElement,
|
||||
RewardType,
|
||||
} from "../types";
|
||||
import { SubstrateEvent } from "@subql/types";
|
||||
import {
|
||||
eventIdFromBlockAndIdxAndAddress,
|
||||
timestamp,
|
||||
eventIdWithAddress,
|
||||
blockNumber,
|
||||
} from "./common";
|
||||
import { Codec } from "@polkadot/types/types";
|
||||
import { u32 } from "@polkadot/types-codec";
|
||||
import { INumber } from "@polkadot/types-codec/types/interfaces";
|
||||
import {
|
||||
PalletNominationPoolsBondedPoolInner,
|
||||
PalletNominationPoolsPoolMember,
|
||||
PalletNominationPoolsSubPools,
|
||||
} from "@polkadot/types/lookup";
|
||||
import {
|
||||
handleGenericForTxHistory,
|
||||
updateAccumulatedGenericReward,
|
||||
} from "./Rewards";
|
||||
import { getPoolMembers } from "./Cache";
|
||||
import { Option } from "@polkadot/types";
|
||||
|
||||
export async function handlePoolReward(
|
||||
rewardEvent: SubstrateEvent<
|
||||
[accountId: Codec, poolId: INumber, reward: INumber]
|
||||
>,
|
||||
): Promise<void> {
|
||||
await handlePoolRewardForTxHistory(rewardEvent);
|
||||
let accumulatedReward = await updateAccumulatedPoolReward(rewardEvent, true);
|
||||
let {
|
||||
event: {
|
||||
data: [accountId, poolId, amount],
|
||||
},
|
||||
} = rewardEvent;
|
||||
await updateAccountPoolRewards(
|
||||
rewardEvent,
|
||||
accountId.toString(),
|
||||
amount.toBigInt(),
|
||||
poolId.toNumber(),
|
||||
RewardType.reward,
|
||||
accumulatedReward.amount,
|
||||
);
|
||||
}
|
||||
|
||||
async function handlePoolRewardForTxHistory(
|
||||
rewardEvent: SubstrateEvent<
|
||||
[accountId: Codec, poolId: INumber, reward: INumber]
|
||||
>,
|
||||
): Promise<void> {
|
||||
const {
|
||||
event: {
|
||||
data: [account, poolId, amount],
|
||||
},
|
||||
} = rewardEvent;
|
||||
handleGenericForTxHistory(
|
||||
rewardEvent,
|
||||
account.toString(),
|
||||
async (element: HistoryElement) => {
|
||||
element.poolReward = {
|
||||
eventIdx: rewardEvent.idx,
|
||||
amount: amount.toString(),
|
||||
isReward: true,
|
||||
poolId: poolId.toNumber(),
|
||||
};
|
||||
return element;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function updateAccumulatedPoolReward(
|
||||
event: SubstrateEvent<[accountId: Codec, poolId: INumber, reward: INumber]>,
|
||||
isReward: boolean,
|
||||
): Promise<AccumulatedReward> {
|
||||
let {
|
||||
event: {
|
||||
data: [accountId, _, amount],
|
||||
},
|
||||
} = event;
|
||||
return await updateAccumulatedGenericReward(
|
||||
AccumulatedPoolReward,
|
||||
accountId.toString(),
|
||||
amount.toBigInt(),
|
||||
isReward,
|
||||
);
|
||||
}
|
||||
|
||||
async function updateAccountPoolRewards(
|
||||
event: SubstrateEvent,
|
||||
accountAddress: string,
|
||||
amount: bigint,
|
||||
poolId: number,
|
||||
rewardType: RewardType,
|
||||
accumulatedAmount: bigint,
|
||||
): Promise<void> {
|
||||
let id = eventIdWithAddress(event, accountAddress);
|
||||
let accountPoolReward = new AccountPoolReward(
|
||||
id,
|
||||
accountAddress,
|
||||
blockNumber(event),
|
||||
timestamp(event.block),
|
||||
amount,
|
||||
accumulatedAmount,
|
||||
rewardType,
|
||||
poolId,
|
||||
);
|
||||
await accountPoolReward.save();
|
||||
}
|
||||
|
||||
export async function handlePoolBondedSlash(
|
||||
bondedSlashEvent: SubstrateEvent<[poolId: INumber, slash: INumber]>,
|
||||
): Promise<void> {
|
||||
const {
|
||||
event: {
|
||||
data: [poolIdEncoded, slash],
|
||||
},
|
||||
} = bondedSlashEvent;
|
||||
const poolId = poolIdEncoded.toNumber();
|
||||
|
||||
const poolOption = (await api.query.nominationPools.bondedPools(
|
||||
poolId,
|
||||
)) as Option<PalletNominationPoolsBondedPoolInner>;
|
||||
const pool = poolOption.unwrap();
|
||||
|
||||
await handleRelaychainPooledStakingSlash(
|
||||
bondedSlashEvent,
|
||||
poolId,
|
||||
pool.points.toBigInt(),
|
||||
slash.toBigInt(),
|
||||
(member: PalletNominationPoolsPoolMember): bigint => {
|
||||
return member.points.toBigInt();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function handlePoolUnbondingSlash(
|
||||
unbondingSlashEvent: SubstrateEvent<
|
||||
[poolId: INumber, era: INumber, slash: INumber]
|
||||
>,
|
||||
): Promise<void> {
|
||||
const {
|
||||
event: {
|
||||
data: [poolId, era, slash],
|
||||
},
|
||||
} = unbondingSlashEvent;
|
||||
const poolIdNumber = poolId.toNumber();
|
||||
const eraIdNumber = era.toNumber();
|
||||
|
||||
const unbondingPools = (
|
||||
(await api.query.nominationPools.subPoolsStorage(
|
||||
poolIdNumber,
|
||||
)) as Option<PalletNominationPoolsSubPools>
|
||||
).unwrap();
|
||||
|
||||
const pool =
|
||||
unbondingPools.withEra.get(eraIdNumber as unknown as u32) ??
|
||||
unbondingPools.noEra;
|
||||
|
||||
await handleRelaychainPooledStakingSlash(
|
||||
unbondingSlashEvent,
|
||||
poolIdNumber,
|
||||
pool.points.toBigInt(),
|
||||
slash.toBigInt(),
|
||||
(member: PalletNominationPoolsPoolMember): bigint => {
|
||||
return (
|
||||
member.unbondingEras.get(eraIdNumber as unknown as u32)?.toBigInt() ??
|
||||
BigInt(0)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function handleRelaychainPooledStakingSlash(
|
||||
event: SubstrateEvent,
|
||||
poolId: number,
|
||||
poolPoints: bigint,
|
||||
slash: bigint,
|
||||
memberPointsCounter: (member: PalletNominationPoolsPoolMember) => bigint,
|
||||
): Promise<void> {
|
||||
if (poolPoints == BigInt(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const members = await getPoolMembers(blockNumber(event));
|
||||
|
||||
for (const [accountId, member] of members) {
|
||||
let memberPoints: bigint;
|
||||
if (member.poolId.toNumber() === poolId) {
|
||||
memberPoints = memberPointsCounter(member);
|
||||
if (memberPoints != BigInt(0)) {
|
||||
const personalSlash = (slash * memberPoints) / poolPoints;
|
||||
|
||||
await handlePoolSlashForTxHistory(
|
||||
event,
|
||||
poolId,
|
||||
accountId,
|
||||
personalSlash,
|
||||
);
|
||||
let accumulatedReward = await updateAccumulatedGenericReward(
|
||||
AccumulatedPoolReward,
|
||||
accountId,
|
||||
personalSlash,
|
||||
false,
|
||||
);
|
||||
await updateAccountPoolRewards(
|
||||
event,
|
||||
accountId,
|
||||
personalSlash,
|
||||
poolId,
|
||||
RewardType.slash,
|
||||
accumulatedReward.amount,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePoolSlashForTxHistory(
|
||||
slashEvent: SubstrateEvent,
|
||||
poolId: number,
|
||||
accountId: string,
|
||||
personalSlash: bigint,
|
||||
): Promise<void> {
|
||||
const extrinsic = slashEvent.extrinsic;
|
||||
const block = slashEvent.block;
|
||||
const blockNumber = block.block.header.number.toString();
|
||||
const blockTimestamp = timestamp(block);
|
||||
const eventId = eventIdFromBlockAndIdxAndAddress(
|
||||
blockNumber,
|
||||
slashEvent.idx.toString(),
|
||||
accountId,
|
||||
);
|
||||
|
||||
const element = HistoryElement.create({
|
||||
id: eventId,
|
||||
timestamp: blockTimestamp,
|
||||
blockNumber: block.block.header.number.toNumber(),
|
||||
extrinsicHash:
|
||||
extrinsic !== undefined ? extrinsic.extrinsic.hash.toString() : null,
|
||||
extrinsicIdx: extrinsic !== undefined ? extrinsic.idx : null,
|
||||
address: accountId,
|
||||
poolReward: {
|
||||
eventIdx: slashEvent.idx,
|
||||
amount: personalSlash.toString(),
|
||||
isReward: false,
|
||||
poolId: poolId,
|
||||
},
|
||||
});
|
||||
|
||||
await element.save();
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
import {
|
||||
AccountReward,
|
||||
AccumulatedReward,
|
||||
HistoryElement,
|
||||
Reward,
|
||||
RewardType,
|
||||
} from "../types";
|
||||
import {
|
||||
SubstrateBlock,
|
||||
SubstrateEvent,
|
||||
SubstrateExtrinsic,
|
||||
} from "@subql/types";
|
||||
import {
|
||||
callsFromBatch,
|
||||
eventIdFromBlockAndIdx,
|
||||
isBatch,
|
||||
timestamp,
|
||||
eventId,
|
||||
eventIdWithAddress,
|
||||
isProxy,
|
||||
callFromProxy,
|
||||
blockNumber,
|
||||
} from "./common";
|
||||
import { CallBase } from "@polkadot/types/types/calls";
|
||||
import { AnyTuple } from "@polkadot/types/types/codec";
|
||||
import { EraIndex } from "@polkadot/types/interfaces/staking";
|
||||
import { Balance, EventRecord } from "@polkadot/types/interfaces";
|
||||
import {
|
||||
cachedRewardDestination,
|
||||
cachedController,
|
||||
cachedStakingRewardEraIndex,
|
||||
} from "./Cache";
|
||||
import { Codec } from "@polkadot/types/types";
|
||||
import { INumber } from "@polkadot/types-codec/types/interfaces";
|
||||
|
||||
function isPayoutStakers(call: CallBase<AnyTuple>): boolean {
|
||||
return call.method == "payoutStakers";
|
||||
}
|
||||
|
||||
function isPayoutStakersByPage(call: CallBase<AnyTuple>): boolean {
|
||||
return call.method == "payoutStakersByPage";
|
||||
}
|
||||
|
||||
function isPayoutValidator(call: CallBase<AnyTuple>): boolean {
|
||||
return call.method == "payoutValidator";
|
||||
}
|
||||
|
||||
function extractArgsFromPayoutStakers(
|
||||
call: CallBase<AnyTuple>,
|
||||
): [string, number] {
|
||||
const [validatorAddressRaw, eraRaw] = call.args;
|
||||
|
||||
return [validatorAddressRaw.toString(), (eraRaw as EraIndex).toNumber()];
|
||||
}
|
||||
|
||||
function extractArgsFromPayoutStakersByPage(
|
||||
call: CallBase<AnyTuple>,
|
||||
): [string, number] {
|
||||
const [validatorAddressRaw, eraRaw, _] = call.args;
|
||||
|
||||
return [validatorAddressRaw.toString(), (eraRaw as EraIndex).toNumber()];
|
||||
}
|
||||
|
||||
function extractArgsFromPayoutValidator(
|
||||
call: CallBase<AnyTuple>,
|
||||
sender: string,
|
||||
): [string, number] {
|
||||
const [eraRaw] = call.args;
|
||||
|
||||
return [sender, (eraRaw as EraIndex).toNumber()];
|
||||
}
|
||||
|
||||
export async function handleRewarded(
|
||||
rewardEvent: SubstrateEvent<[accountId: Codec, reward: INumber]>,
|
||||
): Promise<void> {
|
||||
await handleReward(rewardEvent);
|
||||
}
|
||||
|
||||
export async function handleReward(
|
||||
rewardEvent: SubstrateEvent<[accountId: Codec, reward: INumber]>,
|
||||
): Promise<void> {
|
||||
await handleRewardForTxHistory(rewardEvent);
|
||||
let accumulatedReward = await updateAccumulatedReward(rewardEvent, true);
|
||||
await updateAccountRewards(
|
||||
rewardEvent,
|
||||
RewardType.reward,
|
||||
accumulatedReward.amount,
|
||||
);
|
||||
// let rewardEventId = eventId(rewardEvent)
|
||||
// try {
|
||||
// let errorOccursOnEvent = await ErrorEvent.get(rewardEventId)
|
||||
// if (errorOccursOnEvent !== undefined) {
|
||||
// logger.info(`Skip rewardEvent: ${rewardEventId}`)
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await handleRewardForTxHistory(rewardEvent)
|
||||
// await updateAccumulatedReward(rewardEvent, true)
|
||||
// } catch (error) {
|
||||
// logger.error(`Got error on reward event: ${rewardEventId}: ${error.toString()}`)
|
||||
// let saveError = new ErrorEvent(rewardEventId)
|
||||
// saveError.description = error.toString()
|
||||
// await saveError.save()
|
||||
// }
|
||||
}
|
||||
|
||||
async function handleRewardForTxHistory(
|
||||
rewardEvent: SubstrateEvent,
|
||||
): Promise<void> {
|
||||
let element = await HistoryElement.get(eventId(rewardEvent));
|
||||
|
||||
if (element !== undefined) {
|
||||
// already processed reward previously
|
||||
return;
|
||||
}
|
||||
|
||||
let payoutCallsArgs = rewardEvent.block.block.extrinsics
|
||||
.map((extrinsic) =>
|
||||
determinePayoutCallsArgs(extrinsic.method, extrinsic.signer.toString()),
|
||||
)
|
||||
.filter((args) => args.length != 0)
|
||||
.flat();
|
||||
|
||||
if (payoutCallsArgs.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payoutValidators = payoutCallsArgs.map(([validator]) => validator);
|
||||
|
||||
const initialCallIndex = -1;
|
||||
|
||||
var accountsMapping: { [address: string]: string } = {};
|
||||
|
||||
for (const eventRecord of rewardEvent.block.events) {
|
||||
if (
|
||||
eventRecord.event.section == rewardEvent.event.section &&
|
||||
eventRecord.event.method == rewardEvent.event.method
|
||||
) {
|
||||
let {
|
||||
event: {
|
||||
data: [account, _],
|
||||
},
|
||||
} = eventRecord;
|
||||
|
||||
if (account.toRawType() === "Balance") {
|
||||
return;
|
||||
}
|
||||
|
||||
let accountAddress = account.toString();
|
||||
let rewardDestination = await cachedRewardDestination(
|
||||
accountAddress,
|
||||
eventRecord as unknown as SubstrateEvent,
|
||||
);
|
||||
|
||||
if (rewardDestination.isStaked || rewardDestination.isStash) {
|
||||
accountsMapping[accountAddress] = accountAddress;
|
||||
} else if (rewardDestination.isController) {
|
||||
accountsMapping[accountAddress] = await cachedController(
|
||||
accountAddress,
|
||||
eventRecord as unknown as SubstrateEvent,
|
||||
);
|
||||
} else if (rewardDestination.isAccount) {
|
||||
accountsMapping[accountAddress] =
|
||||
rewardDestination.asAccount.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await buildRewardEvents(
|
||||
rewardEvent.block,
|
||||
rewardEvent.extrinsic,
|
||||
rewardEvent.event.method,
|
||||
rewardEvent.event.section,
|
||||
accountsMapping,
|
||||
initialCallIndex,
|
||||
(currentCallIndex, eventAccount) => {
|
||||
if (payoutValidators.length > currentCallIndex + 1) {
|
||||
const index = payoutValidators.indexOf(eventAccount);
|
||||
return index !== -1 && index > currentCallIndex
|
||||
? index
|
||||
: currentCallIndex;
|
||||
} else {
|
||||
return currentCallIndex;
|
||||
}
|
||||
},
|
||||
(currentCallIndex, eventIdx, stash, amount) => {
|
||||
if (currentCallIndex == -1) {
|
||||
return {
|
||||
eventIdx: eventIdx,
|
||||
amount: amount,
|
||||
isReward: true,
|
||||
stash: stash,
|
||||
validator: "",
|
||||
era: -1,
|
||||
};
|
||||
} else {
|
||||
const [validator, era] = payoutCallsArgs[currentCallIndex];
|
||||
return {
|
||||
eventIdx: eventIdx,
|
||||
amount: amount,
|
||||
isReward: true,
|
||||
stash: stash,
|
||||
validator: validator,
|
||||
era: era,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function determinePayoutCallsArgs(
|
||||
causeCall: CallBase<AnyTuple>,
|
||||
sender: string,
|
||||
): [string, number][] {
|
||||
if (isPayoutStakers(causeCall)) {
|
||||
return [extractArgsFromPayoutStakers(causeCall)];
|
||||
} else if (isPayoutStakersByPage(causeCall)) {
|
||||
return [extractArgsFromPayoutStakersByPage(causeCall)];
|
||||
} else if (isPayoutValidator(causeCall)) {
|
||||
return [extractArgsFromPayoutValidator(causeCall, sender)];
|
||||
} else if (isBatch(causeCall)) {
|
||||
return callsFromBatch(causeCall)
|
||||
.map((call) => {
|
||||
return determinePayoutCallsArgs(call, sender).map(
|
||||
(value, index, array) => {
|
||||
return value;
|
||||
},
|
||||
);
|
||||
})
|
||||
.flat();
|
||||
} else if (isProxy(causeCall)) {
|
||||
let proxyCall = callFromProxy(causeCall);
|
||||
return determinePayoutCallsArgs(proxyCall, sender);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleSlashed(
|
||||
slashEvent: SubstrateEvent<[accountId: Codec, slash: INumber]>,
|
||||
): Promise<void> {
|
||||
await handleSlash(slashEvent);
|
||||
}
|
||||
|
||||
export async function handleSlash(
|
||||
slashEvent: SubstrateEvent<[accountId: Codec, slash: INumber]>,
|
||||
): Promise<void> {
|
||||
await handleSlashForTxHistory(slashEvent);
|
||||
let accumulatedReward = await updateAccumulatedReward(slashEvent, false);
|
||||
await updateAccountRewards(
|
||||
slashEvent,
|
||||
RewardType.slash,
|
||||
accumulatedReward.amount,
|
||||
);
|
||||
// let slashEventId = eventId(slashEvent)
|
||||
// try {
|
||||
// let errorOccursOnEvent = await ErrorEvent.get(slashEventId)
|
||||
// if (errorOccursOnEvent !== undefined) {
|
||||
// logger.info(`Skip slashEvent: ${slashEventId}`)
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await handleSlashForTxHistory(slashEvent)
|
||||
// await updateAccumulatedReward(slashEvent, false)
|
||||
// } catch (error) {
|
||||
// logger.error(`Got error on slash event: ${slashEventId}: ${error.toString()}`)
|
||||
// let saveError = new ErrorEvent(slashEventId)
|
||||
// saveError.description = error.toString()
|
||||
// await saveError.save()
|
||||
// }
|
||||
}
|
||||
|
||||
async function getValidators(era: number): Promise<Set<string>> {
|
||||
const eraStakersInSlashEra = await (api.query.staking.erasStakersClipped
|
||||
? api.query.staking.erasStakersClipped.keys(era)
|
||||
: api.query.staking.erasStakersOverview.keys(era));
|
||||
const validatorsInSlashEra = eraStakersInSlashEra.map((key) => {
|
||||
let [, validatorId] = key.args;
|
||||
|
||||
return validatorId.toString();
|
||||
});
|
||||
return new Set(validatorsInSlashEra);
|
||||
}
|
||||
|
||||
async function handleSlashForTxHistory(
|
||||
slashEvent: SubstrateEvent,
|
||||
): Promise<void> {
|
||||
let element = await HistoryElement.get(eventId(slashEvent));
|
||||
|
||||
if (element !== undefined) {
|
||||
// already processed reward previously
|
||||
return;
|
||||
}
|
||||
const eraWrapped = await api.query.staking.currentEra();
|
||||
const currentEra = Number(eraWrapped.toString());
|
||||
const slashDeferDuration = api.consts.staking.slashDeferDuration;
|
||||
let validatorsSet = new Set();
|
||||
|
||||
const slashEra = !slashDeferDuration
|
||||
? currentEra
|
||||
: currentEra - Number(slashDeferDuration);
|
||||
|
||||
if (
|
||||
api.query.staking.erasStakersOverview ||
|
||||
api.query.staking.erasStakersClipped
|
||||
) {
|
||||
validatorsSet = await getValidators(slashEra);
|
||||
}
|
||||
|
||||
const initialValidator: any = null;
|
||||
|
||||
await buildRewardEvents(
|
||||
slashEvent.block,
|
||||
slashEvent.extrinsic,
|
||||
slashEvent.event.method,
|
||||
slashEvent.event.section,
|
||||
{},
|
||||
initialValidator,
|
||||
(currentValidator, eventAccount) => {
|
||||
return validatorsSet.has(eventAccount) ? eventAccount : currentValidator;
|
||||
},
|
||||
(validator, eventIdx, stash, amount) => {
|
||||
return {
|
||||
eventIdx: eventIdx,
|
||||
amount: amount,
|
||||
isReward: false,
|
||||
stash: stash,
|
||||
validator: validator,
|
||||
era: slashEra,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function buildRewardEvents<A>(
|
||||
block: SubstrateBlock,
|
||||
extrinsic: SubstrateExtrinsic | undefined,
|
||||
eventMethod: String,
|
||||
eventSection: String,
|
||||
accountsMapping: { [address: string]: string },
|
||||
initialInnerAccumulator: A,
|
||||
produceNewAccumulator: (currentAccumulator: A, eventAccount: string) => A,
|
||||
produceReward: (
|
||||
currentAccumulator: A,
|
||||
eventIdx: number,
|
||||
stash: string,
|
||||
amount: string,
|
||||
) => Reward,
|
||||
) {
|
||||
let blockNumber = block.block.header.number.toString();
|
||||
let blockTimestamp = timestamp(block);
|
||||
|
||||
let innerAccumulator = initialInnerAccumulator;
|
||||
for (let eventIndex = 0; eventIndex < block.events.length; eventIndex++) {
|
||||
const eventRecord = block.events[eventIndex];
|
||||
|
||||
if (
|
||||
!(
|
||||
eventRecord.event.method === eventMethod &&
|
||||
eventRecord.event.section === eventSection
|
||||
)
|
||||
)
|
||||
continue;
|
||||
|
||||
let [account, amount] = decodeDataFromReward(
|
||||
eventRecordToSubstrateEvent(eventRecord),
|
||||
);
|
||||
|
||||
innerAccumulator = produceNewAccumulator(
|
||||
innerAccumulator,
|
||||
account.toString(),
|
||||
);
|
||||
|
||||
const eventId = eventIdFromBlockAndIdx(blockNumber, eventIndex.toString());
|
||||
|
||||
const accountAddress = account.toString();
|
||||
const destinationAddress = accountsMapping[accountAddress];
|
||||
|
||||
const element = new HistoryElement(
|
||||
eventId,
|
||||
block.block.header.number.toNumber(),
|
||||
blockTimestamp,
|
||||
destinationAddress !== undefined ? destinationAddress : accountAddress,
|
||||
);
|
||||
|
||||
if (extrinsic !== undefined) {
|
||||
element.extrinsicHash = extrinsic.extrinsic.hash.toString();
|
||||
element.extrinsicIdx = extrinsic.idx;
|
||||
}
|
||||
|
||||
element.reward = produceReward(
|
||||
innerAccumulator,
|
||||
eventIndex,
|
||||
accountAddress,
|
||||
amount.toString(),
|
||||
);
|
||||
|
||||
await element.save();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAccumulatedReward(
|
||||
event: SubstrateEvent,
|
||||
isReward: boolean,
|
||||
): Promise<AccumulatedReward> {
|
||||
let [accountId, amount] = decodeDataFromReward(event);
|
||||
return await updateAccumulatedGenericReward(
|
||||
AccumulatedReward,
|
||||
accountId.toString(),
|
||||
(amount as unknown as Balance).toBigInt(),
|
||||
isReward,
|
||||
);
|
||||
}
|
||||
|
||||
async function updateAccountRewards(
|
||||
event: SubstrateEvent,
|
||||
rewardType: RewardType,
|
||||
accumulatedAmount: bigint,
|
||||
): Promise<void> {
|
||||
let [accountId, amount] = decodeDataFromReward(event);
|
||||
const accountAddress = accountId.toString();
|
||||
let id = eventIdWithAddress(event, accountAddress);
|
||||
let accountReward = new AccountReward(
|
||||
id,
|
||||
accountAddress,
|
||||
blockNumber(event),
|
||||
timestamp(event.block),
|
||||
(amount as unknown as Balance).toBigInt(),
|
||||
accumulatedAmount,
|
||||
rewardType,
|
||||
);
|
||||
await accountReward.save();
|
||||
}
|
||||
|
||||
async function handleParachainRewardForTxHistory(
|
||||
rewardEvent: SubstrateEvent,
|
||||
): Promise<void> {
|
||||
let [account, amount] = decodeDataFromReward(rewardEvent);
|
||||
handleGenericForTxHistory(
|
||||
rewardEvent,
|
||||
account.toString(),
|
||||
async (element: HistoryElement) => {
|
||||
const eraIndex = await cachedStakingRewardEraIndex(rewardEvent);
|
||||
|
||||
const validatorEvent = rewardEvent.block.events.find(
|
||||
(event) =>
|
||||
event.event.section == rewardEvent.event.section &&
|
||||
event.event.method == rewardEvent.event.method,
|
||||
);
|
||||
const validatorId = validatorEvent?.event.data[0].toString();
|
||||
element.reward = {
|
||||
eventIdx: rewardEvent.idx,
|
||||
amount: amount.toString(),
|
||||
isReward: true,
|
||||
stash: account.toString(),
|
||||
validator: validatorId,
|
||||
era: eraIndex,
|
||||
};
|
||||
|
||||
return element;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleParachainRewarded(
|
||||
rewardEvent: SubstrateEvent<[accountId: Codec, reward: INumber]>,
|
||||
): Promise<void> {
|
||||
await handleParachainRewardForTxHistory(rewardEvent);
|
||||
let accumulatedReward = await updateAccumulatedReward(rewardEvent, true);
|
||||
await updateAccountRewards(
|
||||
rewardEvent,
|
||||
RewardType.reward,
|
||||
accumulatedReward.amount,
|
||||
);
|
||||
}
|
||||
|
||||
// ============= Mythos ================
|
||||
|
||||
export async function handleMythosRewarded(
|
||||
rewardEvent: SubstrateEvent<[accountId: Codec, reward: INumber]>,
|
||||
): Promise<void> {
|
||||
await handleMythosRewardForTxHistory(rewardEvent);
|
||||
let accumulatedReward = await updateAccumulatedReward(rewardEvent, true);
|
||||
await updateAccountRewards(
|
||||
rewardEvent,
|
||||
RewardType.reward,
|
||||
accumulatedReward.amount,
|
||||
);
|
||||
}
|
||||
|
||||
async function handleMythosRewardForTxHistory(
|
||||
rewardEvent: SubstrateEvent,
|
||||
): Promise<void> {
|
||||
let [account, amount] = decodeDataFromReward(rewardEvent);
|
||||
|
||||
await handleGenericForTxHistory(
|
||||
rewardEvent,
|
||||
account.toString(),
|
||||
async (element: HistoryElement) => {
|
||||
element.reward = {
|
||||
eventIdx: rewardEvent.idx,
|
||||
amount: amount.toString(),
|
||||
isReward: true,
|
||||
stash: account.toString(),
|
||||
// Mythos staking rewards are paid manually by the user so each reward
|
||||
// aggregates multiple payouts, and it is hard to split it into
|
||||
// individual per-session per-validator pieces
|
||||
validator: null,
|
||||
era: null,
|
||||
};
|
||||
|
||||
return element;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ============= GENERICS ================
|
||||
|
||||
interface AccumulatedInterface {
|
||||
amount: bigint;
|
||||
save(): Promise<void>;
|
||||
}
|
||||
|
||||
interface AccumulatedInterfaceStatic<BaseType extends AccumulatedInterface> {
|
||||
new (id: string, amount: bigint): BaseType;
|
||||
get(accountAddress: string): Promise<BaseType | undefined>;
|
||||
}
|
||||
|
||||
export async function updateAccumulatedGenericReward<
|
||||
AccumulatedRewardType extends AccumulatedInterface,
|
||||
AccumulatedRewardClassType extends
|
||||
AccumulatedInterfaceStatic<AccumulatedRewardType>,
|
||||
>(
|
||||
AccumulatedRewardTypeObject: AccumulatedRewardClassType,
|
||||
accountId: string,
|
||||
amount: bigint,
|
||||
isReward: boolean,
|
||||
): Promise<AccumulatedRewardType> {
|
||||
let accountAddress = accountId;
|
||||
|
||||
let accumulatedReward = await AccumulatedRewardTypeObject.get(accountAddress);
|
||||
if (!accumulatedReward) {
|
||||
accumulatedReward = new AccumulatedRewardTypeObject(
|
||||
accountAddress,
|
||||
BigInt(0),
|
||||
);
|
||||
}
|
||||
accumulatedReward.amount =
|
||||
accumulatedReward.amount + (isReward ? amount : -amount);
|
||||
await accumulatedReward.save();
|
||||
return accumulatedReward;
|
||||
}
|
||||
|
||||
export async function handleGenericForTxHistory(
|
||||
event: SubstrateEvent,
|
||||
address: string,
|
||||
fieldCallback: (element: HistoryElement) => Promise<HistoryElement>,
|
||||
): Promise<void> {
|
||||
const extrinsic = event.extrinsic;
|
||||
const block = event.block;
|
||||
const blockNumber = block.block.header.number.toString();
|
||||
const blockTimestamp = timestamp(block);
|
||||
const eventId = eventIdFromBlockAndIdx(blockNumber, event.idx.toString());
|
||||
|
||||
const element = new HistoryElement(
|
||||
eventId,
|
||||
block.block.header.number.toNumber(),
|
||||
blockTimestamp,
|
||||
address,
|
||||
);
|
||||
if (extrinsic !== undefined) {
|
||||
element.extrinsicHash = extrinsic.extrinsic.hash.toString();
|
||||
element.extrinsicIdx = extrinsic.idx;
|
||||
}
|
||||
|
||||
(await fieldCallback(element)).save();
|
||||
}
|
||||
|
||||
interface AccountRewardsInterface {
|
||||
id: string;
|
||||
|
||||
address: string;
|
||||
|
||||
blockNumber: number;
|
||||
|
||||
timestamp: bigint;
|
||||
|
||||
amount: bigint;
|
||||
|
||||
accumulatedAmount: bigint;
|
||||
|
||||
type: RewardType;
|
||||
save(): Promise<void>;
|
||||
}
|
||||
|
||||
export function eventRecordToSubstrateEvent(
|
||||
eventRecord: EventRecord,
|
||||
): SubstrateEvent {
|
||||
return eventRecord as unknown as SubstrateEvent;
|
||||
}
|
||||
|
||||
function decodeDataFromReward(event: SubstrateEvent): [Codec, Codec] {
|
||||
// In early version staking.Reward data only have 2 parameters [accountId, amount]
|
||||
// Now rewarded changed to https://polkadot.js.org/docs/substrate/events/#rewardedaccountid32-palletstakingrewarddestination-u128
|
||||
// And we can direct access property from data
|
||||
const {
|
||||
event: { data: innerData },
|
||||
} = event;
|
||||
let account: Codec, amount: Codec;
|
||||
if (innerData.length == 2) {
|
||||
[account, amount] = innerData;
|
||||
} else {
|
||||
[account, , amount] = innerData;
|
||||
}
|
||||
return [account, amount];
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import { Codec } from "@polkadot/types/types";
|
||||
import { HistoryElement } from "../types";
|
||||
import { HistoryElementProps } from "../types/models/HistoryElement";
|
||||
import { SubstrateEvent } from "@subql/types";
|
||||
import {
|
||||
blockNumber,
|
||||
eventId,
|
||||
calculateFeeAsString,
|
||||
timestamp,
|
||||
getEventData,
|
||||
isEvmTransaction,
|
||||
isEvmExecutedEvent,
|
||||
isAssetTxFeePaidEvent,
|
||||
isSwapExecutedEvent,
|
||||
eventRecordToSubstrateEvent,
|
||||
getAssetIdFromMultilocation,
|
||||
BigIntFromCodec,
|
||||
convertOrmlCurrencyIdToString,
|
||||
} from "./common";
|
||||
|
||||
type TransferPayload = {
|
||||
event: SubstrateEvent;
|
||||
address: Codec;
|
||||
from: Codec;
|
||||
to: Codec;
|
||||
amount: Codec;
|
||||
suffix: string;
|
||||
assetId?: string;
|
||||
};
|
||||
|
||||
export async function handleSwap(event: SubstrateEvent): Promise<void> {
|
||||
const [from, to, path, amountIn, amountOut] = getEventData(event);
|
||||
|
||||
let element = await HistoryElement.get(`${eventId(event)}-from`);
|
||||
|
||||
if (element !== undefined) {
|
||||
// already processed swap previously
|
||||
return;
|
||||
}
|
||||
|
||||
let assetIdFee: string;
|
||||
let fee: string;
|
||||
let foundAssetTxFeePaid = event.block.events.find((e) =>
|
||||
isAssetTxFeePaidEvent(eventRecordToSubstrateEvent(e)),
|
||||
);
|
||||
let swaps = event.block.events.filter((e) =>
|
||||
isSwapExecutedEvent(eventRecordToSubstrateEvent(e)),
|
||||
);
|
||||
if (foundAssetTxFeePaid === undefined) {
|
||||
assetIdFee = "native";
|
||||
fee = calculateFeeAsString(event.extrinsic, from.toString());
|
||||
} else {
|
||||
const [who, actualFee, tip, rawAssetIdFee] = getEventData(
|
||||
eventRecordToSubstrateEvent(foundAssetTxFeePaid),
|
||||
);
|
||||
assetIdFee = getAssetIdFromMultilocation(rawAssetIdFee);
|
||||
fee = actualFee.toString();
|
||||
|
||||
let {
|
||||
event: {
|
||||
data: [feeFrom, feeTo, feePath, feeAmountIn, feeAmountOut],
|
||||
},
|
||||
} = swaps[0];
|
||||
|
||||
swaps = swaps.slice(1);
|
||||
if (BigIntFromCodec(actualFee) != BigIntFromCodec(feeAmountIn)) {
|
||||
let {
|
||||
event: {
|
||||
data: [
|
||||
refundFrom,
|
||||
refundTo,
|
||||
refundPath,
|
||||
refundAmountIn,
|
||||
refundAmountOut,
|
||||
],
|
||||
},
|
||||
} = swaps[swaps.length - 1];
|
||||
|
||||
if (
|
||||
BigIntFromCodec(feeAmountIn) ==
|
||||
BigIntFromCodec(actualFee) + BigIntFromCodec(refundAmountOut) &&
|
||||
getAssetIdFromMultilocation((feePath as any)[0]) ==
|
||||
getAssetIdFromMultilocation(
|
||||
(refundPath as any)[(refundPath as any)["length"] - 1],
|
||||
)
|
||||
) {
|
||||
swaps = swaps.slice(swaps.length - 1);
|
||||
// TODO: if fee splitted, than we will process the same block two times
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const swap of swaps) {
|
||||
await processSwap(eventRecordToSubstrateEvent(swap), assetIdFee, fee);
|
||||
}
|
||||
}
|
||||
|
||||
async function processSwap(
|
||||
event: SubstrateEvent,
|
||||
assetIdFee: string,
|
||||
fee: string,
|
||||
): Promise<void> {
|
||||
const [from, to, path, amountIn, amountOut] = getEventData(event);
|
||||
|
||||
const swap = {
|
||||
assetIdIn: getAssetIdFromMultilocation((path as any)[0]),
|
||||
amountIn: amountIn.toString(),
|
||||
assetIdOut: getAssetIdFromMultilocation(
|
||||
(path as any)[(path as any)["length"] - 1],
|
||||
),
|
||||
amountOut: amountOut.toString(),
|
||||
sender: from.toString(),
|
||||
receiver: to.toString(),
|
||||
assetIdFee: assetIdFee,
|
||||
fee: fee,
|
||||
eventIdx: event.idx,
|
||||
success: true,
|
||||
};
|
||||
|
||||
await createAssetTransmission(event, from.toString(), "-from", {
|
||||
swap: swap,
|
||||
});
|
||||
if (from.toString() != to.toString()) {
|
||||
await createAssetTransmission(event, to.toString(), "-to", { swap: swap });
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleTransfer(event: SubstrateEvent): Promise<void> {
|
||||
const [from, to, amount] = getEventData(event);
|
||||
|
||||
await createTransfer({
|
||||
event,
|
||||
address: from,
|
||||
from,
|
||||
to,
|
||||
suffix: "-from",
|
||||
amount,
|
||||
});
|
||||
await createTransfer({ event, address: to, from, to, suffix: "-to", amount });
|
||||
}
|
||||
|
||||
export async function handleAssetTransfer(
|
||||
event: SubstrateEvent,
|
||||
): Promise<void> {
|
||||
const [assetId, from, to, amount] = getEventData(event);
|
||||
|
||||
await createTransfer({
|
||||
event,
|
||||
address: from,
|
||||
from,
|
||||
to,
|
||||
suffix: "-from",
|
||||
amount,
|
||||
assetId: assetId.toString(),
|
||||
});
|
||||
await createTransfer({
|
||||
event,
|
||||
address: to,
|
||||
from,
|
||||
to,
|
||||
suffix: "-to",
|
||||
amount,
|
||||
assetId: assetId.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleOrmlTransfer(event: SubstrateEvent): Promise<void> {
|
||||
const [currencyId, from, to, amount] = getEventData(event);
|
||||
|
||||
await createTransfer({
|
||||
event,
|
||||
address: from,
|
||||
from,
|
||||
to,
|
||||
suffix: "-from",
|
||||
amount,
|
||||
assetId: convertOrmlCurrencyIdToString(currencyId),
|
||||
});
|
||||
await createTransfer({
|
||||
event,
|
||||
address: to,
|
||||
from,
|
||||
to,
|
||||
suffix: "-to",
|
||||
amount,
|
||||
assetId: convertOrmlCurrencyIdToString(currencyId),
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleEquilibriumTransfer(
|
||||
event: SubstrateEvent,
|
||||
): Promise<void> {
|
||||
const [from, to, assetId, amount] = getEventData(event);
|
||||
|
||||
await createTransfer({
|
||||
event,
|
||||
address: from,
|
||||
from,
|
||||
to,
|
||||
suffix: "-from",
|
||||
amount,
|
||||
assetId: assetId.toString(),
|
||||
});
|
||||
await createTransfer({
|
||||
event,
|
||||
address: to,
|
||||
from,
|
||||
to,
|
||||
suffix: "-to",
|
||||
amount,
|
||||
assetId: assetId.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleTokenTransfer(
|
||||
event: SubstrateEvent,
|
||||
): Promise<void> {
|
||||
await handleOrmlTransfer(event);
|
||||
}
|
||||
|
||||
export async function handleCurrencyTransfer(
|
||||
event: SubstrateEvent,
|
||||
): Promise<void> {
|
||||
await handleOrmlTransfer(event);
|
||||
}
|
||||
|
||||
async function createTransfer({
|
||||
event,
|
||||
address,
|
||||
suffix,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
assetId = null,
|
||||
}: TransferPayload) {
|
||||
const transfer = {
|
||||
amount: amount.toString(),
|
||||
from: from.toString(),
|
||||
to: to.toString(),
|
||||
fee: calculateFeeAsString(event.extrinsic, from.toString()),
|
||||
eventIdx: event.idx,
|
||||
success: true,
|
||||
};
|
||||
|
||||
let data;
|
||||
if (assetId) {
|
||||
data = {
|
||||
assetTransfer: {
|
||||
...transfer,
|
||||
assetId: assetId,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
data = {
|
||||
transfer: transfer,
|
||||
};
|
||||
}
|
||||
|
||||
await createAssetTransmission(event, address, suffix, data);
|
||||
}
|
||||
|
||||
export async function createAssetTransmission(
|
||||
event: SubstrateEvent,
|
||||
address: any,
|
||||
suffix: string,
|
||||
data: Partial<HistoryElementProps>,
|
||||
) {
|
||||
const element = new HistoryElement(
|
||||
`${eventId(event)}${suffix}`,
|
||||
blockNumber(event),
|
||||
timestamp(event.block),
|
||||
address.toString(),
|
||||
);
|
||||
if (event.extrinsic !== undefined) {
|
||||
if (isEvmTransaction(event.extrinsic.extrinsic.method)) {
|
||||
const executedEvent = event.extrinsic.events.find(isEvmExecutedEvent);
|
||||
element.extrinsicHash =
|
||||
executedEvent?.event.data?.[2]?.toString() ||
|
||||
event.extrinsic.extrinsic.hash.toString();
|
||||
} else {
|
||||
element.extrinsicHash = event.extrinsic.extrinsic.hash.toString();
|
||||
}
|
||||
|
||||
element.extrinsicIdx = event.extrinsic.idx;
|
||||
}
|
||||
|
||||
for (var key in data) {
|
||||
(element[key as keyof HistoryElementProps] as any) =
|
||||
data[key as keyof HistoryElementProps];
|
||||
}
|
||||
|
||||
await element.save();
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
import { SubstrateBlock, SubstrateEvent, TypedEventRecord } from "@subql/types";
|
||||
import { SubstrateExtrinsic } from "@subql/types";
|
||||
import { Balance, EventRecord } from "@polkadot/types/interfaces";
|
||||
import { CallBase } from "@polkadot/types/types/calls";
|
||||
import { AnyTuple, Codec } from "@polkadot/types/types/codec";
|
||||
import { Vec, GenericEventData } from "@polkadot/types";
|
||||
import { INumber } from "@polkadot/types-codec/types/interfaces";
|
||||
import { u8aToHex } from "@polkadot/util";
|
||||
|
||||
const batchCalls = ["batch", "batchAll", "forceBatch"];
|
||||
const transferCalls = ["transfer", "transferKeepAlive"];
|
||||
const ormlSections = ["currencies", "tokens"];
|
||||
|
||||
export function distinct<T>(array: Array<T>): Array<T> {
|
||||
return [...new Set(array)];
|
||||
}
|
||||
|
||||
export function isBatch(call: CallBase<AnyTuple>): boolean {
|
||||
return call.section == "utility" && batchCalls.includes(call.method);
|
||||
}
|
||||
|
||||
export function isProxy(call: CallBase<AnyTuple>): boolean {
|
||||
return call.section == "proxy" && call.method == "proxy";
|
||||
}
|
||||
|
||||
export function isNativeTransfer(call: CallBase<AnyTuple>): boolean {
|
||||
return (
|
||||
(call.section == "balances" && transferCalls.includes(call.method)) ||
|
||||
(call.section == "currencies" && call.method == "transferNativeCurrency")
|
||||
);
|
||||
}
|
||||
|
||||
export function isAssetTransfer(call: CallBase<AnyTuple>): boolean {
|
||||
return call.section == "assets" && transferCalls.includes(call.method);
|
||||
}
|
||||
|
||||
export function isEquilibriumTransfer(call: CallBase<AnyTuple>): boolean {
|
||||
return call.section == "eqBalances" && transferCalls.includes(call.method);
|
||||
}
|
||||
|
||||
export function isEvmTransaction(call: CallBase<AnyTuple>): boolean {
|
||||
return call.section === "ethereum" && call.method === "transact";
|
||||
}
|
||||
|
||||
export function isEvmExecutedEvent(event: TypedEventRecord<Codec[]>): boolean {
|
||||
return (
|
||||
event.event.section === "ethereum" && event.event.method === "Executed"
|
||||
);
|
||||
}
|
||||
|
||||
export function isAssetTxFeePaidEvent(event: SubstrateEvent): boolean {
|
||||
return (
|
||||
event.event.section === "assetTxPayment" &&
|
||||
event.event.method === "AssetTxFeePaid"
|
||||
);
|
||||
}
|
||||
|
||||
export function isCurrencyDepositedEvent(event: SubstrateEvent): boolean {
|
||||
return (
|
||||
event.event.section === "currencies" && event.event.method === "Deposited"
|
||||
);
|
||||
}
|
||||
|
||||
export function isSwapExecutedEvent(event: SubstrateEvent): boolean {
|
||||
return (
|
||||
event.event.section === "assetConversion" &&
|
||||
event.event.method === "SwapExecuted"
|
||||
);
|
||||
}
|
||||
|
||||
export function isSwapExactTokensForTokens(call: CallBase<AnyTuple>): boolean {
|
||||
return (
|
||||
call.section === "assetConversion" &&
|
||||
call.method === "swapExactTokensForTokens"
|
||||
);
|
||||
}
|
||||
|
||||
export function isSwapTokensForExactTokens(call: CallBase<AnyTuple>): boolean {
|
||||
return (
|
||||
call.section === "assetConversion" &&
|
||||
call.method === "swapTokensForExactTokens"
|
||||
);
|
||||
}
|
||||
|
||||
export function isHydraOmnipoolBuy(call: CallBase<AnyTuple>): boolean {
|
||||
return call.section === "omnipool" && call.method == "buy";
|
||||
}
|
||||
|
||||
export function isHydraOmnipoolSell(call: CallBase<AnyTuple>): boolean {
|
||||
return call.section === "omnipool" && call.method == "sell";
|
||||
}
|
||||
|
||||
export function isHydraRouterBuy(call: CallBase<AnyTuple>): boolean {
|
||||
return call.section === "router" && call.method == "buy";
|
||||
}
|
||||
|
||||
export function isHydraRouterSell(call: CallBase<AnyTuple>): boolean {
|
||||
return call.section === "router" && call.method == "sell";
|
||||
}
|
||||
|
||||
export function isOrmlTransfer(call: CallBase<AnyTuple>): boolean {
|
||||
return (
|
||||
ormlSections.includes(call.section) && transferCalls.includes(call.method)
|
||||
);
|
||||
}
|
||||
|
||||
export function isNativeTransferAll(call: CallBase<AnyTuple>): boolean {
|
||||
return call.section == "balances" && call.method === "transferAll";
|
||||
}
|
||||
|
||||
export function isOrmlTransferAll(call: CallBase<AnyTuple>): boolean {
|
||||
return ormlSections.includes(call.section) && call.method === "transferAll";
|
||||
}
|
||||
|
||||
export function callsFromBatch(
|
||||
batchCall: CallBase<AnyTuple>,
|
||||
): CallBase<AnyTuple>[] {
|
||||
return batchCall.args[0] as Vec<CallBase<AnyTuple>>;
|
||||
}
|
||||
|
||||
export function callFromProxy(
|
||||
proxyCall: CallBase<AnyTuple>,
|
||||
): CallBase<AnyTuple> {
|
||||
return proxyCall.args[2] as CallBase<AnyTuple>;
|
||||
}
|
||||
|
||||
export function eventIdWithAddress(
|
||||
event: SubstrateEvent,
|
||||
address: String,
|
||||
): string {
|
||||
return `${eventId(event)}-${address}`;
|
||||
}
|
||||
|
||||
export function eventId(event: SubstrateEvent): string {
|
||||
return `${blockNumber(event)}-${event.idx}`;
|
||||
}
|
||||
|
||||
export function eventIdFromBlockAndIdx(blockNumber: string, eventIdx: string) {
|
||||
return `${blockNumber}-${eventIdx}`;
|
||||
}
|
||||
|
||||
export function eventIdFromBlockAndIdxAndAddress(
|
||||
blockNumber: string,
|
||||
eventIdx: string,
|
||||
address: string,
|
||||
) {
|
||||
return `${blockNumber}-${eventIdx}-${address}`;
|
||||
}
|
||||
|
||||
export function extrinsicIdx(event: SubstrateEvent): string {
|
||||
let idx: string = event.extrinsic
|
||||
? event.extrinsic.idx.toString()
|
||||
: event.idx.toString();
|
||||
return idx;
|
||||
}
|
||||
|
||||
export function blockNumber(event: SubstrateEvent): number {
|
||||
return event.block.block.header.number.toNumber();
|
||||
}
|
||||
|
||||
export function extrinsicIdFromBlockAndIdx(
|
||||
blockNumber: number,
|
||||
extrinsicIdx: number,
|
||||
): string {
|
||||
return `${blockNumber.toString()}-${extrinsicIdx.toString()}`;
|
||||
}
|
||||
|
||||
export function timestamp(block: SubstrateBlock): bigint {
|
||||
return BigInt(
|
||||
Math.round(block.timestamp ? block.timestamp.getTime() / 1000 : -1),
|
||||
);
|
||||
}
|
||||
|
||||
export function calculateFeeAsString(
|
||||
extrinsic?: SubstrateExtrinsic,
|
||||
from: string = "",
|
||||
): string {
|
||||
if (extrinsic) {
|
||||
const transactionPaymentFee =
|
||||
exportFeeFromTransactionFeePaidEvent(extrinsic);
|
||||
|
||||
if (transactionPaymentFee != undefined) {
|
||||
return transactionPaymentFee.toString();
|
||||
}
|
||||
|
||||
const withdrawFee = exportFeeFromBalancesWithdrawEvent(extrinsic, from);
|
||||
|
||||
if (withdrawFee !== BigInt(0)) {
|
||||
if (isEvmTransaction(extrinsic.extrinsic.method)) {
|
||||
const feeRefund = exportFeeRefund(extrinsic, from);
|
||||
return feeRefund
|
||||
? (withdrawFee - feeRefund).toString()
|
||||
: withdrawFee.toString();
|
||||
}
|
||||
return withdrawFee.toString();
|
||||
}
|
||||
|
||||
let balancesFee = exportFeeFromBalancesDepositEvent(extrinsic);
|
||||
let treasureFee = exportFeeFromTreasureDepositEvent(extrinsic);
|
||||
|
||||
let totalFee = balancesFee + treasureFee;
|
||||
return totalFee.toString();
|
||||
} else {
|
||||
return BigInt(0).toString();
|
||||
}
|
||||
}
|
||||
|
||||
export function getEventData(event: SubstrateEvent): GenericEventData {
|
||||
return event.event.data as GenericEventData;
|
||||
}
|
||||
|
||||
export function eventRecordToSubstrateEvent(
|
||||
eventRecord: EventRecord,
|
||||
): SubstrateEvent {
|
||||
return eventRecord as unknown as SubstrateEvent;
|
||||
}
|
||||
|
||||
export function BigIntFromCodec(eventRecord: Codec): bigint {
|
||||
return (eventRecord as unknown as INumber).toBigInt();
|
||||
}
|
||||
|
||||
export function convertOrmlCurrencyIdToString(currencyId: Codec): string {
|
||||
// make sure first we have scale encoded bytes
|
||||
const bytes = currencyId.toU8a();
|
||||
|
||||
return u8aToHex(bytes).toString();
|
||||
}
|
||||
|
||||
function exportFeeRefund(
|
||||
extrinsic: SubstrateExtrinsic,
|
||||
from: string = "",
|
||||
): bigint {
|
||||
const extrinsicSigner = from || extrinsic.extrinsic.signer.toString();
|
||||
|
||||
const eventRecord = extrinsic.events.find(
|
||||
(event) =>
|
||||
event.event.method == "Deposit" &&
|
||||
event.event.section == "balances" &&
|
||||
event.event.data[0].toString() === extrinsicSigner,
|
||||
);
|
||||
|
||||
if (eventRecord != undefined) {
|
||||
const {
|
||||
event: {
|
||||
data: [, fee],
|
||||
},
|
||||
} = eventRecord;
|
||||
|
||||
return (fee as unknown as Balance).toBigInt();
|
||||
}
|
||||
|
||||
return BigInt(0);
|
||||
}
|
||||
|
||||
function exportFeeFromBalancesWithdrawEvent(
|
||||
extrinsic: SubstrateExtrinsic,
|
||||
from: string = "",
|
||||
): bigint {
|
||||
const eventRecord = extrinsic.events.find(
|
||||
(event) =>
|
||||
event.event.method == "Withdraw" && event.event.section == "balances",
|
||||
);
|
||||
|
||||
if (eventRecord !== undefined) {
|
||||
const {
|
||||
event: {
|
||||
data: [accountid, fee],
|
||||
},
|
||||
} = eventRecord;
|
||||
|
||||
const extrinsicSigner = from || extrinsic.extrinsic.signer.toString();
|
||||
const withdrawAccountId = accountid.toString();
|
||||
return extrinsicSigner === withdrawAccountId
|
||||
? (fee as unknown as Balance).toBigInt()
|
||||
: BigInt(0);
|
||||
}
|
||||
|
||||
return BigInt(0);
|
||||
}
|
||||
|
||||
function exportFeeFromTransactionFeePaidEvent(
|
||||
extrinsic: SubstrateExtrinsic,
|
||||
from: string = "",
|
||||
): bigint | undefined {
|
||||
const eventRecord = extrinsic.events.find(
|
||||
(event) =>
|
||||
event.event.method == "TransactionFeePaid" &&
|
||||
event.event.section == "transactionPayment",
|
||||
);
|
||||
|
||||
if (eventRecord !== undefined) {
|
||||
const {
|
||||
event: {
|
||||
data: [accountid, fee, tip],
|
||||
},
|
||||
} = eventRecord;
|
||||
|
||||
const fullFee = (fee as Balance).toBigInt() + (tip as Balance).toBigInt();
|
||||
|
||||
const extrinsicSigner = from || extrinsic.extrinsic.signer.toString();
|
||||
const withdrawAccountId = accountid.toString();
|
||||
return extrinsicSigner === withdrawAccountId ? fullFee : undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function exportFeeFromBalancesDepositEvent(
|
||||
extrinsic: SubstrateExtrinsic,
|
||||
): bigint {
|
||||
const eventRecord = extrinsic.events.find((event) => {
|
||||
return event.event.method == "Deposit" && event.event.section == "balances";
|
||||
});
|
||||
|
||||
if (eventRecord != undefined) {
|
||||
const {
|
||||
event: {
|
||||
data: [, fee],
|
||||
},
|
||||
} = eventRecord;
|
||||
|
||||
return (fee as unknown as Balance).toBigInt();
|
||||
}
|
||||
|
||||
return BigInt(0);
|
||||
}
|
||||
|
||||
function exportFeeFromTreasureDepositEvent(
|
||||
extrinsic: SubstrateExtrinsic,
|
||||
): bigint {
|
||||
const eventRecord = extrinsic.events.find((event) => {
|
||||
return event.event.method == "Deposit" && event.event.section == "treasury";
|
||||
});
|
||||
|
||||
if (eventRecord != undefined) {
|
||||
const {
|
||||
event: {
|
||||
data: [fee],
|
||||
},
|
||||
} = eventRecord;
|
||||
|
||||
return (fee as unknown as Balance).toBigInt();
|
||||
} else {
|
||||
return BigInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAssetIdFromMultilocation(
|
||||
multilocation: any,
|
||||
safe = false,
|
||||
): string | undefined {
|
||||
try {
|
||||
let junctions = multilocation.interior;
|
||||
|
||||
if (junctions.isHere) {
|
||||
return "native";
|
||||
} else if (multilocation.parents != "0") {
|
||||
return multilocation.toHex();
|
||||
} else {
|
||||
return junctions.asX2[1].asGeneralIndex.toString();
|
||||
}
|
||||
} catch (e) {
|
||||
if (safe) {
|
||||
return undefined;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getRewardData(event: SubstrateEvent): [Codec, Codec] {
|
||||
const {
|
||||
event: { data: innerData },
|
||||
} = event;
|
||||
let account: Codec, amount: Codec;
|
||||
if (innerData.length == 2) {
|
||||
[account, amount] = innerData;
|
||||
} else {
|
||||
[account, , amount] = innerData;
|
||||
}
|
||||
return [account, amount];
|
||||
}
|
||||
|
||||
export function extractTransactionPaidFee(
|
||||
events: EventRecord[],
|
||||
): string | undefined {
|
||||
const eventRecord = events.find(
|
||||
(event) =>
|
||||
event.event.method == "TransactionFeePaid" &&
|
||||
event.event.section == "transactionPayment",
|
||||
);
|
||||
|
||||
if (eventRecord == undefined) return undefined;
|
||||
|
||||
const {
|
||||
event: {
|
||||
data: [_, fee, tip],
|
||||
},
|
||||
} = eventRecord;
|
||||
|
||||
const fullFee = (fee as Balance).toBigInt() + (tip as Balance).toBigInt();
|
||||
|
||||
return fullFee.toString();
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { SubstrateEvent } from "@subql/types";
|
||||
import {
|
||||
BigIntFromCodec,
|
||||
calculateFeeAsString,
|
||||
eventId,
|
||||
eventRecordToSubstrateEvent,
|
||||
getAssetIdFromMultilocation,
|
||||
getEventData,
|
||||
isAssetTxFeePaidEvent,
|
||||
isSwapExecutedEvent,
|
||||
} from "../common";
|
||||
import { HistoryElement } from "../../types";
|
||||
import { createAssetTransmission } from "../Transfers";
|
||||
|
||||
export async function handleAssetConversionSwap(
|
||||
event: SubstrateEvent
|
||||
): Promise<void> {
|
||||
const [from, to, path, amountIn, amountOut] = getEventData(event);
|
||||
|
||||
let element = await HistoryElement.get(`${eventId(event)}-from`);
|
||||
|
||||
if (element !== undefined) {
|
||||
// already processed swap previously
|
||||
return;
|
||||
}
|
||||
|
||||
let assetIdFee: string;
|
||||
let fee: string;
|
||||
let foundAssetTxFeePaid = event.block.events.find((e) =>
|
||||
isAssetTxFeePaidEvent(eventRecordToSubstrateEvent(e))
|
||||
);
|
||||
let swaps = event.block.events.filter((e) =>
|
||||
isSwapExecutedEvent(eventRecordToSubstrateEvent(e))
|
||||
);
|
||||
if (foundAssetTxFeePaid === undefined) {
|
||||
assetIdFee = "native";
|
||||
fee = calculateFeeAsString(event.extrinsic, from.toString());
|
||||
} else {
|
||||
const [who, actualFee, tip, rawAssetIdFee] = getEventData(
|
||||
eventRecordToSubstrateEvent(foundAssetTxFeePaid)
|
||||
);
|
||||
assetIdFee = getAssetIdFromMultilocation(rawAssetIdFee);
|
||||
fee = actualFee.toString();
|
||||
|
||||
let {
|
||||
event: {
|
||||
data: [feeFrom, feeTo, feePath, feeAmountIn, feeAmountOut],
|
||||
},
|
||||
} = swaps[0];
|
||||
|
||||
swaps = swaps.slice(1);
|
||||
if (BigIntFromCodec(actualFee) != BigIntFromCodec(feeAmountIn)) {
|
||||
let {
|
||||
event: {
|
||||
data: [
|
||||
refundFrom,
|
||||
refundTo,
|
||||
refundPath,
|
||||
refundAmountIn,
|
||||
refundAmountOut,
|
||||
],
|
||||
},
|
||||
} = swaps[swaps.length - 1];
|
||||
|
||||
const feePathArray = feePath as unknown as any[];
|
||||
const refundPathArray = refundPath as unknown as any[];
|
||||
|
||||
if (
|
||||
BigIntFromCodec(feeAmountIn) ==
|
||||
BigIntFromCodec(actualFee) + BigIntFromCodec(refundAmountOut) &&
|
||||
getAssetIdFromMultilocation(feePathArray[0]) ==
|
||||
getAssetIdFromMultilocation(
|
||||
refundPathArray[refundPathArray["length"] - 1]
|
||||
)
|
||||
) {
|
||||
swaps = swaps.slice(swaps.length - 1);
|
||||
// TODO: if fee splitted, than we will process the same block two times
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const e of swaps) {
|
||||
await processAssetConversionSwap(
|
||||
eventRecordToSubstrateEvent(e),
|
||||
assetIdFee,
|
||||
fee
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function processAssetConversionSwap(
|
||||
event: SubstrateEvent,
|
||||
assetIdFee: string,
|
||||
fee: string
|
||||
): Promise<void> {
|
||||
const [from, to, path, amountIn, amountOut] = getEventData(event);
|
||||
|
||||
const pathArray = path as unknown as any[];
|
||||
|
||||
const swap = {
|
||||
assetIdIn: getAssetIdFromMultilocation(pathArray[0]),
|
||||
amountIn: amountIn.toString(),
|
||||
assetIdOut: getAssetIdFromMultilocation(pathArray[pathArray["length"] - 1]),
|
||||
amountOut: amountOut.toString(),
|
||||
sender: from.toString(),
|
||||
receiver: to.toString(),
|
||||
assetIdFee: assetIdFee,
|
||||
fee: fee,
|
||||
eventIdx: event.idx,
|
||||
success: true,
|
||||
};
|
||||
|
||||
await createAssetTransmission(event, from.toString(), "-from", {
|
||||
swap: swap,
|
||||
});
|
||||
if (from.toString() != to.toString()) {
|
||||
await createAssetTransmission(event, to.toString(), "-to", { swap: swap });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { SubstrateEvent, TypedEventRecord } from "@subql/types";
|
||||
import {
|
||||
eventId,
|
||||
eventRecordToSubstrateEvent,
|
||||
extractTransactionPaidFee,
|
||||
isCurrencyDepositedEvent,
|
||||
convertOrmlCurrencyIdToString,
|
||||
} from "../common";
|
||||
import { HistoryElement } from "../../types";
|
||||
import { createAssetTransmission } from "../Transfers";
|
||||
import { AccountId32 } from "@polkadot/types/interfaces/runtime";
|
||||
import { u128, u32 } from "@polkadot/types-codec";
|
||||
import { EventRecord } from "@polkadot/types/interfaces";
|
||||
import { Codec } from "@polkadot/types/types";
|
||||
import { INumber } from "@polkadot/types-codec/types/interfaces";
|
||||
|
||||
type OmnipoolSwapArgs = [
|
||||
who: AccountId32,
|
||||
assetIn: u32,
|
||||
assetOut: u32,
|
||||
amountIn: u128,
|
||||
amountOut: u128,
|
||||
assetFeeAmount: u128,
|
||||
protocolFeeAmount: u128,
|
||||
];
|
||||
|
||||
type RouterSwapArgs = [
|
||||
assetIn: u32,
|
||||
assetOut: u32,
|
||||
amountIn: u128,
|
||||
amountOut: u128,
|
||||
];
|
||||
|
||||
export async function handleOmnipoolSwap(
|
||||
event: SubstrateEvent<OmnipoolSwapArgs>,
|
||||
): Promise<void> {
|
||||
let element = await HistoryElement.get(`${eventId(event)}-from`);
|
||||
if (element !== undefined) {
|
||||
// already processed swap previously
|
||||
return;
|
||||
}
|
||||
if (event.extrinsic == undefined) {
|
||||
// TODO we dont yet process swap events that were initiated by the system and not by the user
|
||||
// Example: https://hydradx.subscan.io/block/4361343?tab=event&event=4361343-27
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPartOfRouterSwap(event.extrinsic.events)) {
|
||||
// TODO: we currently don't support swaps in batch
|
||||
return;
|
||||
}
|
||||
|
||||
const fee = findHydraDxFeeTyped(event.extrinsic.events);
|
||||
const [who, assetIn, assetOut, amountIn, amountOut] = event.event.data;
|
||||
|
||||
const swap = {
|
||||
assetIdIn: convertHydraDxTokenIdToString(assetIn),
|
||||
amountIn: amountIn.toString(),
|
||||
assetIdOut: convertHydraDxTokenIdToString(assetOut),
|
||||
amountOut: amountOut.toString(),
|
||||
sender: who.toString(),
|
||||
receiver: who.toString(),
|
||||
assetIdFee: fee.tokenId,
|
||||
fee: fee.amount,
|
||||
eventIdx: event.idx,
|
||||
success: true,
|
||||
};
|
||||
|
||||
const blockNumber = event.block.block.header.number;
|
||||
logger.info(
|
||||
`Constructed omnipool swap ${JSON.stringify(
|
||||
swap,
|
||||
)} for block ${blockNumber.toString()}`,
|
||||
);
|
||||
|
||||
await createAssetTransmission(event, who.toString(), "-from", { swap: swap });
|
||||
}
|
||||
|
||||
export async function handleHydraRouterSwap(
|
||||
event: SubstrateEvent<RouterSwapArgs>,
|
||||
): Promise<void> {
|
||||
let element = await HistoryElement.get(`${eventId(event)}-from`);
|
||||
if (element !== undefined) {
|
||||
return;
|
||||
}
|
||||
if (event.extrinsic == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const who = event.extrinsic.extrinsic.signer.toString();
|
||||
const fee = findHydraDxFeeTyped(event.extrinsic.events);
|
||||
const [assetIn, assetOut, amountIn, amountOut] = event.event.data;
|
||||
|
||||
const swap = {
|
||||
assetIdIn: convertHydraDxTokenIdToString(assetIn),
|
||||
amountIn: amountIn.toString(),
|
||||
assetIdOut: convertHydraDxTokenIdToString(assetOut),
|
||||
amountOut: amountOut.toString(),
|
||||
sender: who.toString(),
|
||||
receiver: who.toString(),
|
||||
assetIdFee: fee.tokenId,
|
||||
fee: fee.amount,
|
||||
eventIdx: event.idx,
|
||||
success: true,
|
||||
};
|
||||
|
||||
const blockNumber = event.block.block.header.number;
|
||||
logger.info(
|
||||
`Constructed router swap ${JSON.stringify(
|
||||
swap,
|
||||
)} for block ${blockNumber.toString()}`,
|
||||
);
|
||||
|
||||
await createAssetTransmission(event, who.toString(), "-from", { swap: swap });
|
||||
}
|
||||
|
||||
export type Fee = {
|
||||
tokenId: string;
|
||||
amount: string;
|
||||
};
|
||||
|
||||
export function findHydraDxFeeTyped(events: TypedEventRecord<Codec[]>[]): Fee {
|
||||
return findHydraDxFee(events as EventRecord[]);
|
||||
}
|
||||
|
||||
export function findHydraDxFee(events: EventRecord[]): Fee {
|
||||
const lastCurrenciesDepositEvent = findLastEvent(events, (event) =>
|
||||
isCurrencyDepositedEvent(eventRecordToSubstrateEvent(event)),
|
||||
);
|
||||
|
||||
if (lastCurrenciesDepositEvent == undefined) return findNativeFee(events);
|
||||
|
||||
const {
|
||||
event: {
|
||||
data: [currencyId, _, amount],
|
||||
},
|
||||
} = lastCurrenciesDepositEvent;
|
||||
|
||||
return {
|
||||
tokenId: convertHydraDxTokenIdToString(currencyId),
|
||||
amount: (amount as INumber).toString(),
|
||||
};
|
||||
}
|
||||
|
||||
function isPartOfRouterSwap(events: TypedEventRecord<Codec[]>[]): boolean {
|
||||
const eventRecords = events as EventRecord[];
|
||||
for (const eventRecord of eventRecords) {
|
||||
if (
|
||||
eventRecord.event.section == "router" &&
|
||||
(eventRecord.event.method == "Executed" ||
|
||||
eventRecord.event.method == "RouteExecuted")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function findNativeFee(events: EventRecord[]): Fee {
|
||||
let foundAssetTxFeePaid = extractTransactionPaidFee(events);
|
||||
if (foundAssetTxFeePaid == undefined) foundAssetTxFeePaid = "0";
|
||||
|
||||
return {
|
||||
tokenId: "native",
|
||||
amount: foundAssetTxFeePaid,
|
||||
};
|
||||
}
|
||||
|
||||
export function convertHydraDxTokenIdToString(hydraDxTokenId: Codec): string {
|
||||
const asString = hydraDxTokenId.toString();
|
||||
|
||||
if (asString == "0") {
|
||||
return "native";
|
||||
} else {
|
||||
return convertOrmlCurrencyIdToString(hydraDxTokenId);
|
||||
}
|
||||
}
|
||||
|
||||
function findLastEvent(
|
||||
events: EventRecord[],
|
||||
expression: (event: EventRecord) => boolean,
|
||||
): EventRecord | undefined {
|
||||
const currenciesDepositedEvents = events.filter(expression);
|
||||
|
||||
if (currenciesDepositedEvents.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return currenciesDepositedEvents[currenciesDepositedEvents.length - 1];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//Exports all handler functions
|
||||
export * from "./AssetConversion";
|
||||
export * from "./HydraDx";
|
||||
import "@polkadot/api-augment";
|
||||
Reference in New Issue
Block a user