Skip to content

Commit

Permalink
feat: optional basic flattener
Browse files Browse the repository at this point in the history
  • Loading branch information
mfornos committed Jul 26, 2024
1 parent 42eda89 commit d486492
Show file tree
Hide file tree
Showing 15 changed files with 309 additions and 96 deletions.
20 changes: 14 additions & 6 deletions packages/core/src/operators/flatten.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@
import { Observable, concatMap } from 'rxjs'

import { TxWithIdAndEvent } from '../types/interfaces.js'
import { Flattener } from './flatten/flattener.js'
import { hasParser } from './flatten/index.js'
import { createFlattener, isNested } from './flatten/index.js'

function withFlattener(tx: TxWithIdAndEvent, sorted: boolean) {
export enum FlattenerMode {
BASIC,
CORRELATED,
}

export { Flattener, createFlattener } from './flatten/index.js'

function withFlattener(tx: TxWithIdAndEvent, mode: FlattenerMode, sorted: boolean) {
try {
const flattener = new Flattener(tx)
const flattener = createFlattener(tx, mode)

flattener.flatten()

return sorted
? flattener.flattenedCalls.sort((a: TxWithIdAndEvent, b: TxWithIdAndEvent) => {
return (a.levelId ?? '0').localeCompare(b.levelId ?? '0')
Expand All @@ -34,8 +42,8 @@ function withFlattener(tx: TxWithIdAndEvent, sorted: boolean) {
*
* @param sorted - (Optional) preserve the order of nested calls. Defaults to true.
*/
export function flattenCalls(sorted = true) {
export function flattenCalls(mode = FlattenerMode.CORRELATED, sorted = true) {
return (source: Observable<TxWithIdAndEvent>): Observable<TxWithIdAndEvent> => {
return source.pipe(concatMap((tx) => (hasParser(tx) ? withFlattener(tx, sorted) : [tx])))
return source.pipe(concatMap((tx) => (isNested(tx) ? withFlattener(tx, mode, sorted) : [tx])))
}
}
32 changes: 32 additions & 0 deletions packages/core/src/operators/flatten/basic/flattener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { TxWithIdAndEvent } from '../../../types/interfaces.js'
import { Flattener } from '../interfaces.js'
import { findParser } from './index.js'

export class BasicFlattener implements Flattener {
private calls: TxWithIdAndEvent[]
private tx: TxWithIdAndEvent

constructor(tx: TxWithIdAndEvent) {
this.tx = tx
this.calls = []
}

flatten(id = '0') {
this.tx.levelId = id
this.calls.push(this.tx)

const parser = findParser(this.tx)

if (parser) {
const nestedCalls = parser(this.tx)
for (let i = nestedCalls.length - 1; i >= 0; i--) {
this.tx = nestedCalls[i]
this.flatten(`${id}.${i}`)
}
}
}

get flattenedCalls(): TxWithIdAndEvent[] {
return this.calls
}
}
22 changes: 22 additions & 0 deletions packages/core/src/operators/flatten/basic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { TxWithIdAndEvent } from '../../../types/interfaces.js'

import { extractAsMultiCall } from './multisig.js'
import { extractProxyCalls } from './proxy.js'
import { extractAnyBatchCalls, extractAsDerivativeCall } from './utility.js'

type CallParser = (tx: TxWithIdAndEvent) => TxWithIdAndEvent[]

const parsers: Record<string, CallParser> = {
'proxy.proxy': extractProxyCalls,
'proxy.proxyAnnounced': extractProxyCalls,
'multisig.asMulti': extractAsMultiCall,
'multisig.asMultiThreshold1': extractAsMultiCall,
'utility.batch': extractAnyBatchCalls,
'utility.batchAll': extractAnyBatchCalls,
'utility.forceBatch': extractAnyBatchCalls,
'utility.asDerivative': extractAsDerivativeCall,
}

export function findParser({ extrinsic: { method } }: TxWithIdAndEvent): CallParser | undefined {
return parsers[`${method.section}.${method.method}`]
}
25 changes: 25 additions & 0 deletions packages/core/src/operators/flatten/basic/multisig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Call } from '@polkadot/types/interfaces/runtime'

import { TxWithIdAndEvent } from '../../../types/interfaces.js'
import { callAsTx, getArgValueFromTx, getMultisigAddres } from '../util.js'

export function extractAsMultiCall(tx: TxWithIdAndEvent) {
let extraSigner
try {
const multisigAddress = getMultisigAddres(tx.extrinsic)
extraSigner = {
type: 'multisig',
address: multisigAddress,
}
} catch {
//
}
const call = getArgValueFromTx(tx.extrinsic, 'call') as Call
return [
callAsTx({
call,
tx,
extraSigner,
}),
]
}
19 changes: 19 additions & 0 deletions packages/core/src/operators/flatten/basic/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MultiAddress } from '@polkadot/types/interfaces'
import type { Call } from '@polkadot/types/interfaces/runtime'

import { TxWithIdAndEvent } from '../../../types/interfaces.js'
import { callAsTx, getArgValueFromTx } from '../util.js'

export function extractProxyCalls(tx: TxWithIdAndEvent) {
const { extrinsic } = tx
const real = getArgValueFromTx(extrinsic, 'real') as MultiAddress
const call = getArgValueFromTx(extrinsic, 'call') as Call

return [
callAsTx({
call,
tx,
extraSigner: { type: 'proxied', address: real },
}),
]
}
29 changes: 29 additions & 0 deletions packages/core/src/operators/flatten/basic/utility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { u16 } from '@polkadot/types-codec'

import type { AnyTuple, CallBase } from '@polkadot/types-codec/types'
import type { FunctionMetadataLatest } from '@polkadot/types/interfaces'

import { TxWithIdAndEvent } from '../../../types/interfaces.js'
import { callAsTx } from '../util.js'

export function extractAnyBatchCalls(tx: TxWithIdAndEvent) {
const { extrinsic } = tx
const calls = extrinsic.args[0] as unknown as CallBase<AnyTuple, FunctionMetadataLatest>[]
return calls.map((call) =>
callAsTx({
call,
tx,
})
)
}

export function extractAsDerivativeCall(tx: TxWithIdAndEvent) {
const [_, call] = tx.extrinsic.args as unknown as [u16, CallBase<AnyTuple, FunctionMetadataLatest>]

return [
callAsTx({
call,
tx,
}),
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import { logger } from '@polkadot/util'

import type { DispatchError, DispatchInfo } from '@polkadot/types/interfaces'

import { GenericExtrinsicWithId } from '../../types/index.js'
import { EventWithId, TxWithIdAndEvent } from '../../types/interfaces.js'
import { GenericExtrinsicWithId } from '../../../types/index.js'
import { EventWithId, TxWithIdAndEvent } from '../../../types/interfaces.js'
import { Flattener } from '../interfaces.js'
import { isEventType } from '../util.js'
import { findParser } from './index.js'
import { isEventType } from './util.js'

const l = logger('oc-ops-flatten')

const MAX_EVENTS = 200
const l = logger('oc-ops-flattener')

/**
* Enum representing static, well-known boundaries.
Expand Down Expand Up @@ -49,7 +48,7 @@ const isAllBoundary = (boundary: Boundary): boundary is Boundaries => {
* Flattens nested calls in the extrinsic and correlates the events belonging to each call.
* Supports all the extractors registered in the {@link parsers} map.
*/
export class Flattener {
export class CorrelatedFlattener implements Flattener {
private events: {
event: EventWithId
callId: number
Expand All @@ -62,11 +61,6 @@ export class Flattener {
const { extrinsic } = tx
const { registry } = extrinsic

if (tx.events.length > MAX_EVENTS) {
l.warn(`Number of events (${tx.events.length}) in tx exceeds max limit of ${MAX_EVENTS}. Skipping flatten...`)
throw new Error(`Number of events (${tx.events.length}) in tx exceeds max limit of ${MAX_EVENTS}`)
}

// work on a copy of the events and extrinsics
this.events = tx.events
.slice()
Expand Down
49 changes: 49 additions & 0 deletions packages/core/src/operators/flatten/correlated/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2023-2024 SO/DA zone
// SPDX-License-Identifier: Apache-2.0

import { TxWithIdAndEvent } from '../../../types/interfaces.js'
import { Boundary, CorrelatedFlattener } from './flattener.js'
import { extractAsMultiCall, extractAsMutiThreshold1Call } from './multisig.js'
import { extractProxyCalls } from './proxy.js'
import { extractAsDerivativeCall, extractBatchAllCalls, extractBatchCalls, extractForceBatchCalls } from './utility.js'

/**
* Type that represents an extractor function.
*/
type CallParser = (
tx: TxWithIdAndEvent,
flattener: CorrelatedFlattener
) => {
call: TxWithIdAndEvent
boundary?: Boundary
}[]

/**
* Parsers object which maps method signatures to their corresponding extractor functions.
* Extractor functions take a transaction as input and return the nested call(s)
* as an array of transactions, a single transaction, or undefined based on the extraction logic.
*/
export const parsers: Record<string, CallParser> = {
'proxy.proxy': extractProxyCalls,
'proxy.proxyAnnounced': extractProxyCalls,
'multisig.asMulti': extractAsMultiCall,
'multisig.asMultiThreshold1': extractAsMutiThreshold1Call,
'utility.batch': extractBatchCalls,
'utility.batchAll': extractBatchAllCalls,
'utility.forceBatch': extractForceBatchCalls,
'utility.asDerivative': extractAsDerivativeCall,
}

/**
* Returns a call parser matching the extrinsic call name or undefined.
*/
export function findParser({ extrinsic: { method } }: TxWithIdAndEvent): CallParser | undefined {
return parsers[`${method.section}.${method.method}`]
}

/**
* Returns true if a parser exists for the given extrinsic call name.
*/
export function hasParser(tx: TxWithIdAndEvent): boolean {
return findParser(tx) !== undefined
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

import type { Null, Result } from '@polkadot/types-codec'
import type { Vec } from '@polkadot/types-codec'
import { AccountId32, DispatchError } from '@polkadot/types/interfaces'
import type { Address, Call } from '@polkadot/types/interfaces/runtime'
import { isU8a, u8aToHex } from '@polkadot/util'
import { createKeyMulti } from '@polkadot/util-crypto'

import { TxWithIdAndEvent } from '../../types/interfaces.js'
import { Boundaries, Flattener } from './flattener.js'
import { callAsTxWithBoundary, getArgValueFromEvent, getArgValueFromTx } from './util.js'
import { TxWithIdAndEvent } from '../../../types/interfaces.js'
import { callAsTxWithBoundary, getArgValueFromEvent, getArgValueFromTx, getMultisigAddres } from '../util.js'
import { Boundaries, CorrelatedFlattener } from './flattener.js'

const MultisigExecuted = 'multisig.MultisigExecuted'
const MultisigExecutedBoundary = {
Expand All @@ -32,7 +29,7 @@ const MultisigExecutedBoundary = {
* @returns The extracted multisig call as TxWithIdAndEvent.
* Returns undefined if the 'MultisigExecuted' event is not found in the transaction events.
*/
export function extractAsMultiCall(tx: TxWithIdAndEvent, flattener: Flattener) {
export function extractAsMultiCall(tx: TxWithIdAndEvent, flattener: CorrelatedFlattener) {
const { extrinsic } = tx

const multisigExecutedIndex = flattener.findEventIndex(MultisigExecuted)
Expand Down Expand Up @@ -74,16 +71,7 @@ export function extractAsMultiCall(tx: TxWithIdAndEvent, flattener: Flattener) {
* @returns The extracted multisig call as TxWithIdAndEvent.
*/
export function extractAsMutiThreshold1Call(tx: TxWithIdAndEvent) {
const { extrinsic } = tx
const otherSignatories = getArgValueFromTx(tx.extrinsic, 'other_signatories') as Vec<AccountId32>
// Signer must be added to the signatories to obtain the multisig address
const signatories = otherSignatories.map((s) => s.toString())
signatories.push(extrinsic.signer.toString())
const multisig = createKeyMulti(signatories, 1)
const multisigAddress = extrinsic.registry.createTypeUnsafe('Address', [
isU8a(multisig) ? u8aToHex(multisig) : multisig,
]) as Address

const multisigAddress = getMultisigAddres(tx.extrinsic)
const call = getArgValueFromTx(tx.extrinsic, 'call') as Call

return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import type { Null, Result } from '@polkadot/types-codec'
import { DispatchError, MultiAddress } from '@polkadot/types/interfaces'
import type { Call } from '@polkadot/types/interfaces/runtime'

import { TxWithIdAndEvent } from '../../types/interfaces.js'
import { Flattener } from './flattener.js'
import { callAsTxWithBoundary, getArgValueFromTx } from './util.js'
import { TxWithIdAndEvent } from '../../../types/interfaces.js'
import { callAsTxWithBoundary, getArgValueFromTx } from '../util.js'
import { CorrelatedFlattener } from './flattener.js'

const ProxyExecuted = 'proxy.ProxyExecuted'
const ProxyExecutedBoundary = {
Expand All @@ -22,7 +22,7 @@ const ProxyExecutedBoundary = {
* @param tx - The input transaction to extract proxy calls from .
* @returns The extracted proxy call as TxWithIdAndEvent.
*/
export function extractProxyCalls(tx: TxWithIdAndEvent, flattener: Flattener) {
export function extractProxyCalls(tx: TxWithIdAndEvent, flattener: CorrelatedFlattener) {
const { extrinsic } = tx
const real = getArgValueFromTx(extrinsic, 'real') as MultiAddress
const call = getArgValueFromTx(extrinsic, 'call') as Call
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import type { Null, Result } from '@polkadot/types-codec'
import type { AnyTuple, CallBase } from '@polkadot/types-codec/types'
import type { DispatchError, FunctionMetadataLatest } from '@polkadot/types/interfaces'

import { TxWithIdAndEvent } from '../../types/interfaces.js'
import { Boundaries, Flattener } from './flattener.js'
import { callAsTxWithBoundary, getArgValueFromTx, isEventType } from './util.js'
import { TxWithIdAndEvent } from '../../../types/interfaces.js'
import { callAsTxWithBoundary, getArgValueFromTx, isEventType } from '../util.js'
import { Boundaries, CorrelatedFlattener } from './flattener.js'

const MAX_BATCH_CALLS = 50

Expand Down Expand Up @@ -36,13 +36,13 @@ const DispatchedAsBoundary = {
*
* @param calls - Array of batch calls.
* @param tx - The original transaction.
* @param flattener - The {@link Flattener} instance.
* @param flattener - The {@link CorrelatedFlattener} instance.
* @returns An array of batch calls mapped as {@link TxWithIdAndEvent}.
*/
function mapBatchErrored(
calls: CallBase<AnyTuple, FunctionMetadataLatest>[],
tx: TxWithIdAndEvent,
flattener: Flattener
flattener: CorrelatedFlattener
) {
let from = flattener.nextPointer

Expand Down Expand Up @@ -91,10 +91,10 @@ export function extractAsDerivativeCall(tx: TxWithIdAndEvent) {
* maps the execution result from the event to the extracted call.
*
* @param tx - The 'dispatchAs' transaction.
* @param flattener - The {@link Flattener} instance.
* @param flattener - The {@link CorrelatedFlattener} instance.
* @returns The extracted call as {@link TxWithIdAndEvent}.
*/
export function extractDispatchAsCall(tx: TxWithIdAndEvent, flattener: Flattener) {
export function extractDispatchAsCall(tx: TxWithIdAndEvent, flattener: CorrelatedFlattener) {
const { extrinsic, events } = tx
const call = getArgValueFromTx(extrinsic, 'call') as CallBase<AnyTuple, FunctionMetadataLatest>

Expand Down Expand Up @@ -165,11 +165,11 @@ function mapBatchInterrupt(
* 'BatchCompleted' event is emitted if all items are executed succesfully, otherwise emits 'BatchInterrupted'
*
* @param tx - The 'utility.batch' transaction.
* @param flattener - The {@link Flattener} instance.
* @param flattener - The {@link CorrelatedFlattener} instance.
* @returns The array of extracted batch calls
* with correlated events and dispatch result as {@link TxWithIdAndEvent}.
*/
export function extractBatchCalls(tx: TxWithIdAndEvent, flattener: Flattener) {
export function extractBatchCalls(tx: TxWithIdAndEvent, flattener: CorrelatedFlattener) {
const { extrinsic } = tx
const calls = extrinsic.args[0] as unknown as CallBase<AnyTuple, FunctionMetadataLatest>[]
if (calls.length > MAX_BATCH_CALLS) {
Expand Down Expand Up @@ -223,11 +223,11 @@ export function extractBatchAllCalls(tx: TxWithIdAndEvent) {
* If some items fails, will emit ItemFailed for failed items, ItemCompleted for successful items, and BatchCompletedWithErrors at the end.
*
* @param tx - The transaction with ID and events.
* @param flattener - The {@link Flattener} instance.
* @param flattener - The {@link CorrelatedFlattener} instance.
* @returns The array of extracted batch calls
* with correlated events and dispatch result as {@link TxWithIdAndEvent}.
*/
export function extractForceBatchCalls(tx: TxWithIdAndEvent, flattener: Flattener) {
export function extractForceBatchCalls(tx: TxWithIdAndEvent, flattener: CorrelatedFlattener) {
const { extrinsic } = tx
const calls = extrinsic.args[0] as unknown as CallBase<AnyTuple, FunctionMetadataLatest>[]
if (calls.length > MAX_BATCH_CALLS) {
Expand Down
Loading

0 comments on commit d486492

Please sign in to comment.