Skip to content

Commit

Permalink
Sketch out the chain service
Browse files Browse the repository at this point in the history
The service sets up providers and listens for incoming transactions for
all tracked accounts. It doesn't yet save transactions or expose account
or transaction data to the rest of the backend.

All account tracking has been extracted from the indexing service, but
no account state has been extracted from the existing accounts
controllers.
  • Loading branch information
mhluongo committed Aug 15, 2021
1 parent 8b86185 commit bbf5664
Show file tree
Hide file tree
Showing 12 changed files with 401 additions and 61 deletions.
2 changes: 1 addition & 1 deletion api/constants/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BTC, ETH } from "./currencies"
// TODO integrate this with /api/networks

export const ETHEREUM = {
name: "Ethereum Main Net",
name: "Ethereum",
baseAsset: ETH,
chainID: "1",
family: "EVM",
Expand Down
14 changes: 13 additions & 1 deletion api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
startService as startIndexing,
IndexingService,
} from "./services/indexing"
import { startService as startChain, ChainService } from "./services/chain"

// import { Keys } from "./keys"

Expand Down Expand Up @@ -47,6 +48,13 @@ class Main {
*/
preferenceService: Promise<PreferenceService>

/*
* A promise to the chain service, keeping track of base asset balances,
* transactions, and network status. The promise will be resolved when the
* service is initialized.
*/
chainService: Promise<ChainService>

/*
* A promise to the indexing service, keeping track of token balances and
* prices. The promise will be resolved when the service is initialized.
Expand Down Expand Up @@ -85,7 +93,11 @@ class Main {

async initializeServices() {
this.preferenceService = startPreferences()
this.indexingService = startIndexing(this.preferenceService)
this.chainService = startChain(this.preferenceService)
this.indexingService = startIndexing(
this.preferenceService,
this.chainService
)
}

/*
Expand Down
14 changes: 3 additions & 11 deletions api/lib/erc20.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { AlchemyProvider, BaseProvider } from "@ethersproject/providers"
import { ethers } from "ethers"
import { ETHEREUM } from "../constants"
import { AccountBalance, SmartContractFungibleAsset } from "../types"

const ALCHEMY_KEY = "8R4YNuff-Is79CeEHM2jzj2ssfzJcnfa"

/*
* Get an account's balance from an ERC20-compliant contract.
*/
export async function getBalance(
provider: BaseProvider,
tokenAddress: string,
account: string
): Promise<BigInt> {
const provider = new ethers.providers.AlchemyProvider(
{ name: "homestead", chainId: 1 },
ALCHEMY_KEY
)
const abi = ["function balanceOf(address owner) view returns (uint256)"]
const token = new ethers.Contract(tokenAddress, abi, provider)

Expand All @@ -27,14 +23,10 @@ export async function getBalance(
* If no token contracts are provided, no balances will be returned.
*/
export async function getBalances(
provider: AlchemyProvider,
tokens: SmartContractFungibleAsset[],
account: string
): Promise<AccountBalance[]> {
const provider = new ethers.providers.AlchemyProvider(
{ name: "homestead", chainId: 1 },
ALCHEMY_KEY
)

if (tokens.length === 0) {
return [] as AccountBalance[]
}
Expand Down
2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"watch": "webpack --mode=development --watch"
},
"dependencies": {
"@ethersproject/networks": "^5.4.2",
"@ethersproject/providers": "^5.4.3",
"@ethersproject/transactions": "^5.4.0",
"@ethersproject/web": "^5.4.0",
"@uniswap/token-lists": "^1.0.0-beta.25",
Expand Down
151 changes: 151 additions & 0 deletions api/services/chain/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import Dexie from "dexie"

import {
AccountBalance,
AccountNetwork,
FungibleAsset,
Network,
} from "../../types"

// TODO application data atop transactions (eg token balances)

export interface Transaction {
hash: string
from: string
to: string
gas: BigInt
gasPrice: BigInt
input: string
nonce: BigInt
value: BigInt
dataSource: "local" | "alchemy"
network: Network
}

export interface ConfirmedTransaction extends Transaction {
blockHash: string
blockNumber: string
}

export interface SignedTransaction extends Transaction {
r: string
s: string
v: string
}

export interface SignedConfirmedTransaction
extends SignedTransaction,
ConfirmedTransaction {}

export interface Migration {
id: number
appliedAt: number
}

export class ChainDatabase extends Dexie {
/*
* Accounts whose transaction and balances should be tracked on a particular
* network.
*/
accountsToTrack: Dexie.Table<AccountNetwork, number>

/*
*
*/
transactions: Dexie.Table<
| Transaction
| ConfirmedTransaction
| SignedTransaction
| SignedConfirmedTransaction,
number
>

/*
* Historic account balances.
*/
balances: Dexie.Table<AccountBalance, number>

migrations: Dexie.Table<Migration, number>

constructor() {
super("tally/chain")
this.version(1).stores({
migrations: "++id,appliedAt",
accountsToTrack:
"++id,account,network.family,network.chainID,network.name",
balances:
"++id,account,assetAmount.amount,assetAmount.asset.symbol,network.name,blockHeight,retrievedAt",
transactions:
"&[hash+network.name],hash,from,[from+network.name],to,[to+network.name],nonce,[nonce+from+network.name],blockHash,blockNumber,network.name",
})
}

async getTransaction(
network: Network,
txHash: string
): Promise<
| Transaction
| ConfirmedTransaction
| SignedTransaction
| SignedConfirmedTransaction
| null
> {
return (
(
await this.transactions
.where("[hash+network.name]")
.equals([txHash, network.name])
.toArray()
)[0] || null
)
}

async getLatestAccountBalance(
account: string,
network: Network,
asset: FungibleAsset
): Promise<AccountBalance | null> {
// TODO this needs to be tightened up, both for performance and specificity
const balanceCandidates = await this.balances
.where("retrievedAt")
.above(Date.now() - 7 * 24 * 60 * 60 * 1000)
.filter(
(balance) =>
balance.account === account &&
balance.assetAmount.asset.symbol === asset.symbol &&
balance.network.name === network.name
)
.reverse()
.sortBy("retrievedAt")
return balanceCandidates.length > 0 ? balanceCandidates[0] : null
}

async setAccountsToTrack(
accountAndNetworks: AccountNetwork[]
): Promise<void> {
await this.transaction("rw", this.accountsToTrack, () => {
this.accountsToTrack.clear()
this.accountsToTrack.bulkAdd(accountAndNetworks)
})
}

async getAccountsToTrack(): Promise<AccountNetwork[]> {
return this.accountsToTrack.toArray()
}
}

export async function getDB(): Promise<ChainDatabase> {
return new ChainDatabase()
}

export async function getOrCreateDB(): Promise<ChainDatabase> {
const db = await getDB()
const numMigrations = await db.migrations.count()
if (numMigrations === 0) {
await db.transaction("rw", db.migrations, async () => {
db.migrations.add({ id: 0, appliedAt: Date.now() })
// TODO decide migrations before the initial release
})
}
return db
}
11 changes: 11 additions & 0 deletions api/services/chain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import PreferenceService from "../preferences/service"
import ChainService from "./service"
export { default as ChainService } from "./service"

export async function startService(
preferenceService: Promise<PreferenceService>
): Promise<ChainService> {
const service = new ChainService(preferenceService)
await service.startService()
return service
}

0 comments on commit bbf5664

Please sign in to comment.