Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/aleph-holders/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@aleph-indexer/bsc": "^1.1.11",
"@aleph-indexer/core": "^1.1.10",
"@aleph-indexer/ethereum": "^1.1.11",
"@aleph-indexer/solana": "^1.1.11",
"@aleph-indexer/framework": "^1.1.11"
}
}
2 changes: 1 addition & 1 deletion packages/aleph-holders/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async function main() {

const projectId = config.INDEXER_NAMESPACE || 'aleph-holders'
const supportedBlockchains = (
config.INDEXER_BLOCKCHAINS || 'ethereum,bsc'
config.INDEXER_BLOCKCHAINS || 'ethereum,bsc,solana'
).split(',') as Blockchain[]
const dataPath = config.INDEXER_DATA_PATH || undefined // 'data'
const transport =
Expand Down
4 changes: 2 additions & 2 deletions packages/aleph-holders/src/api/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from 'graphql'
import { GraphQLBlockchain } from '@aleph-indexer/framework'

export const ERC20TransferEventQueryArgs = {
export const TransferEventQueryArgs = {
blockchain: { type: new GraphQLNonNull(GraphQLBlockchain) },
account: { type: GraphQLString },
startDate: { type: GraphQLFloat },
Expand All @@ -19,7 +19,7 @@ export const ERC20TransferEventQueryArgs = {
reverse: { type: GraphQLBoolean },
}

export const ERC20BalanceQueryArgs = {
export const BalanceQueryArgs = {
blockchain: { type: new GraphQLNonNull(GraphQLBlockchain) },
account: { type: GraphQLString },
limit: { type: GraphQLInt },
Expand Down
13 changes: 8 additions & 5 deletions packages/aleph-holders/src/api/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ import MainDomain from '../domain/main.js'
import {
Balance,
BalanceQueryArgs,
ERC20TransferEvent,
ERC20TransferEventQueryArgs,
Snapshot,
TransferEvent,
TransferEventQueryArgs,
} from '../types.js'

export class APIResolver {
constructor(protected domain: MainDomain) {}

async getEvents(
args: ERC20TransferEventQueryArgs,
): Promise<ERC20TransferEvent[]> {
async getEvents(args: TransferEventQueryArgs): Promise<TransferEvent[]> {
return this.domain.getEvents(args)
}

async getBalances(args: BalanceQueryArgs): Promise<Balance[]> {
return this.domain.getBalances(args)
}

async getSnapshot(timestamp?: number): Promise<Snapshot> {
return this.domain.getSnapshot(timestamp)
}
}
23 changes: 16 additions & 7 deletions packages/aleph-holders/src/api/schema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { GraphQLObjectType } from 'graphql'
import { GraphQLInt, GraphQLObjectType } from 'graphql'
import { IndexerAPISchema } from '@aleph-indexer/framework'
import * as Types from './types.js'
import * as Args from './args.js'
import { APIResolver } from './resolvers.js'
import MainDomain from '../domain/main.js'
import { BalanceQueryArgs, ERC20TransferEventQueryArgs } from '../types.js'
import { BalanceQueryArgs, TransferEventQueryArgs } from '../types.js'
import { GraphQLLong } from '@aleph-indexer/core'

export default class APISchema extends IndexerAPISchema {
constructor(
Expand All @@ -18,17 +19,25 @@ export default class APISchema extends IndexerAPISchema {
name: 'Query',
fields: {
events: {
type: Types.ERC20TransferEventList,
args: Args.ERC20TransferEventQueryArgs,
type: Types.TransferEventList,
args: Args.TransferEventQueryArgs,
resolve: (_, args) =>
this.resolver.getEvents(args as ERC20TransferEventQueryArgs),
this.resolver.getEvents(args as TransferEventQueryArgs),
},
balances: {
type: Types.ERC20BalanceList,
args: Args.ERC20BalanceQueryArgs,
type: Types.BalanceList,
args: Args.BalanceQueryArgs,
resolve: (_, args) =>
this.resolver.getBalances(args as BalanceQueryArgs),
},
snapshot: {
type: Types.Snapshot,
args: {
timestamp: { type: GraphQLLong },
},
resolve: (_, args) =>
this.resolver.getSnapshot(args.timestamp),
},
},
}),
})
Expand Down
34 changes: 27 additions & 7 deletions packages/aleph-holders/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const commonFields = {
transaction: { type: new GraphQLNonNull(GraphQLString) },
}

export const ERC20TransferEvent = new GraphQLObjectType({
name: 'ERC20TransferEvent',
export const TransferEvent = new GraphQLObjectType({
name: 'TransferEvent',
fields: {
...commonFields,
from: { type: new GraphQLNonNull(GraphQLString) },
Expand All @@ -27,17 +27,37 @@ export const ERC20TransferEvent = new GraphQLObjectType({
},
})

export const ERC20TransferEventList = new GraphQLList(ERC20TransferEvent)
export const TransferEventList = new GraphQLList(TransferEvent)

export const ERC20Balance = new GraphQLObjectType({
name: 'ERC20Balance',
export const Balance = new GraphQLObjectType({
name: 'Balance',
Comment on lines -32 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be still a ERC20Balance, as we have a Solana specific implementation down below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

balance query also can return solana balances from snapshot db

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now where the problem in this discussion lies.

What you can do is make Balance a union of two types, ERC20Balance and SolanaBalance:

const erc20BalanceFields = {
  account: { type: new GraphQLNonNull(GraphQLString) },
  balance: { type: new GraphQLNonNull(GraphQLBigNumber) },
  balanceNum: { type: GraphQLFloat },
}

const solanaBalanceFields = {
  ...erc20BalanceFields,
  tokenAccount: { type: new GraphQLNonNull(GraphQLString) },
}

export const ERC20Balance = new GraphQLObjectType({
  name: 'ERC20Balance',
  fields: erc20BalanceFields,
})

export const SolanaBalance = new GraphQLObjectType({
  name: 'SolanaBalance',
  fields: solanaBalanceFields,
})

export const Balance = new GraphQLUnionType({
  name: 'Balance',
  types: [ ERC20Balance, SolanaBalance],
});

This will allow you to keep all the relevant info for Solana-related balances, while not having to deal with undefined values for ERC20Balances. This style also comes in handy later, when you might want to filter balances based on which type they are.

fields: {
account: { type: new GraphQLNonNull(GraphQLString) },
balance: { type: new GraphQLNonNull(GraphQLBigNumber) },
balanceNum: { type: GraphQLFloat },
},
})

export const ERC20BalanceList = new GraphQLList(ERC20Balance)
export const BalanceList = new GraphQLList(Balance)

export const types = [ERC20TransferEvent, ERC20Balance]
export const SolanaBalance = new GraphQLObjectType({
name: 'SolanaBalance',
fields: {
account: { type: new GraphQLNonNull(GraphQLString) },
owner: { type: new GraphQLNonNull(GraphQLString) },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparing this to the previous ERC20Balance, the account fields mean two different things:

  • For ERC20 it is the owner account
  • For Solana is is the ATA

For clarification I'd prefer if the owner account stays the account and instead we call the ATA the tokenAccount

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay i can change that, then balances query should return tokenAccount or owner? it seems more straightforward to return the tokenAccount as this is where the tokens are stored. I can implement a query to return token account data also.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tokenAccount is an implementation detail that is not relevant to (most) users. Provide the information, but don't make it the primary identifier. The owner address is much more relevant to most usecases.

balance: { type: new GraphQLNonNull(GraphQLBigNumber) },
balanceNum: { type: GraphQLFloat },
},
})

export const SolanaBalanceList = new GraphQLList(SolanaBalance)

export const Snapshot = new GraphQLObjectType({
name: 'Snapshot',
fields: {
timestamp: { type: new GraphQLNonNull(GraphQLString) },
balances: { type: new GraphQLNonNull(SolanaBalanceList) },
},
})

export const types = [TransferEvent, Balance, Snapshot]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solana types are not being exported in this list, is this intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snapshot is the 'solana type', balances query also returns solana balances data

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels a bit off to me... Are ERC20 balances not capable of snapshots? Why even collect snapshots then, if we could just try to index the current balance at any time?

4 changes: 2 additions & 2 deletions packages/aleph-holders/src/dal/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export enum BalanceDALIndex {

const accountKey = {
get: (e: Balance) => e.account,
length: EntityStorage.EthereumAddressLength,
length: EntityStorage.AddressLength,
}

const blockchainKey = {
Expand Down Expand Up @@ -49,7 +49,7 @@ const mapValueFn = async (value: any) => {

export function createBalanceDAL(path: string): BalanceStorage {
return new EntityStorage<Balance>({
name: 'erc20_balance',
name: 'balance',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we are storing Balance (previously ERC20Balance) in this database. What about Solana balances and their additional field then? Are they not stored?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snapshot taken is stored on the snapshot dal

path,
key: [blockchainKey, accountKey],
indexes: [
Expand Down
17 changes: 17 additions & 0 deletions packages/aleph-holders/src/dal/snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { EntityStorage } from '@aleph-indexer/core'
import { Snapshot } from '../types.js'

export type SnapshotStorage = EntityStorage<Snapshot>

const timestampKey = {
get: (e: Snapshot) => e.timestamp,
length: EntityStorage.TimestampLength,
}

export function createSnapshotDAL(path: string): SnapshotStorage {
return new EntityStorage<Snapshot>({
name: 'snapshot',
path,
key: [timestampKey],
})
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,44 @@
import { EntityStorage } from '@aleph-indexer/core'
import { ERC20TransferEvent } from '../types.js'
import { TransferEvent } from '../types.js'
import {
blockchainDecimals,
uint256ToBigNumber,
uint256ToNumber,
} from '../utils/index.js'

export type ERC20TransferEventStorage = EntityStorage<ERC20TransferEvent>
export type TransferEventStorage = EntityStorage<TransferEvent>

export enum ERC20TransferEventDALIndex {
export enum TransferEventDALIndex {
BlockchainTimestamp = 'blockchain_timestamp',
BlockchainHeight = 'blockchain_height',
BlockchainAccountTimestamp = 'blockchain_account_timestamp',
BlockchainAccountHeight = 'blockchain_account_height',
}

const idKey = {
get: (e: ERC20TransferEvent) => e.id,
get: (e: TransferEvent) => e.id,
length: EntityStorage.VariableLength,
}

const accountKey = {
get: (e: ERC20TransferEvent) => [e.from, e.to],
length: EntityStorage.EthereumAddressLength,
get: (e: TransferEvent) => [e.from, e.to],
length: EntityStorage.AddressLength,
}

const blockchainKey = {
get: (e: ERC20TransferEvent) => e.blockchain,
get: (e: TransferEvent) => e.blockchain,
length: EntityStorage.VariableLength,
}

const timestampKey = {
get: (e: ERC20TransferEvent) => e.timestamp,
get: (e: TransferEvent) => e.timestamp,
length: EntityStorage.TimestampLength,
}

const heightKey = {
get: (e: ERC20TransferEvent) => e.height,
get: (e: TransferEvent) => e.height,
// @note: up to 10**9 [9 digits] enough for 300 years in ethereum
// on solana 14 years aprox, block gen each 400ms
length: 8,
}

Expand All @@ -60,28 +61,26 @@ const mapValueFn = async (value: any) => {
return value
}

export function createERC20TransferEventDAL(
path: string,
): ERC20TransferEventStorage {
return new EntityStorage<ERC20TransferEvent>({
name: 'erc20_transfer_event',
export function createTransferEventDAL(path: string): TransferEventStorage {
return new EntityStorage<TransferEvent>({
name: 'transfer_event',
path,
key: [idKey],
indexes: [
{
name: ERC20TransferEventDALIndex.BlockchainTimestamp,
name: TransferEventDALIndex.BlockchainTimestamp,
key: [blockchainKey, timestampKey],
},
{
name: ERC20TransferEventDALIndex.BlockchainHeight,
name: TransferEventDALIndex.BlockchainHeight,
key: [blockchainKey, heightKey],
},
{
name: ERC20TransferEventDALIndex.BlockchainAccountTimestamp,
name: TransferEventDALIndex.BlockchainAccountTimestamp,
key: [blockchainKey, accountKey, timestampKey],
},
{
name: ERC20TransferEventDALIndex.BlockchainAccountHeight,
name: TransferEventDALIndex.BlockchainAccountHeight,
key: [blockchainKey, accountKey, heightKey],
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { BlockchainId } from '@aleph-indexer/framework'
import { EthereumParsedLog } from '@aleph-indexer/ethereum'
import { AlephEvent, Balance, ERC20TransferEvent } from '../types.js'
import { AlephEvent, Balance, TransferEvent } from '../types.js'
import {
bigNumberToString,
uint256ToBigNumber,
uint256ToString,
} from '../utils/index.js'

export class EventParser {
export class EthereumEventParser {
parseERC20TransferEvent(
blockchain: BlockchainId,
entity: EthereumParsedLog,
): ERC20TransferEvent {
): TransferEvent {
const parsedEvent = this.parseCommonScheme(blockchain, entity)

const [rawFrom, rawTo, rawValue] = entity.parsed?.args || []
Expand Down
Loading