Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: BCH using chronik #843

Merged
merged 18 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
git_hook_setup = cp .githooks/pre-commit .git/hooks/pre-commit
git_diff_to_master = git diff --name-only --diff-filter=ACMRTUXB origin/master > DIFF
create_test_paybutton_json = echo { \"priceAPIURL\": \"foo\", \"networkBlockchainClients\": { \"ecash\": \"chronik\", \"bitcoincash\": \"grpc\" }, \"chronikClientURL\": \"https://xec.paybutton.io\", \"wsBaseURL\": \"localhost:5000\" } > paybutton-config.json
create_test_paybutton_json = echo { \"priceAPIURL\": \"foo\", \"networkBlockchainClients\": { \"ecash\": \"chronik\", \"bitcoincash\": \"chronik\" }, \"networkBlockchainURLs\": { \"ecash\": \"https://xec.paybutton.io\", \"bitcoincash\": \"https://chronik.pay2stay.com/bch\" }, \"wsBaseURL\": \"localhost:5000\" } > paybutton-config.json
touch_local_env = touch .env.local

prod:
Expand Down
34 changes: 10 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,15 @@ default: false,
> If the connection of test networks for eCash and Bitcoin Cash should appear in the Networks tab.


#### grpcBCHNodeURL
#### networkBlockchainURLs
```
type: string
default: "bchd.greyh.at:8335"
```
> GRPC URL to connect to for BCH (unsupported at the moment).


#### grpcXECNodeURL
```
type: string
default: "grpc.fabien.cash:8335"
```
> GRPC URL to connect to for XEC (unsupported at the moment).

type: {
"ecash": "https://chronik.fabien.cash",
"bitcoincash": "https://chronik.pay2stay.com/bch"
}

#### chronikClientURL
```
type: string
default: "https://chronik.fabien.cash"
```
> URL for the Chronik client to connect to. Providing an array of URLs is supported.
> What URLs to connect each network chosen client to (from networkBlockchainClients)


#### priceAPIURL
Expand All @@ -114,16 +101,15 @@ default: "redis://paybutton-cache:6379"
#### networkBlockchainClients
```
type: {
"ecash": "chronik" | "grpc"
"bitcoincash": "grpc"
"ecash": "chronik",
"bitcoincash": "chronik"
}
default: {
"ecash": "chronik",
"bitcoincash": "grpc"
"bitcoincash": "chronik"
}
```
> Which client to use to get the blockchain information for each network. Currently, only "chronik" is supported for eCash
and Bitcoin Cash is not supported.
> Which client to use to get the blockchain information for each network. Currently, only "chronik" is supported for eCash and Bitcoin Cash.


#### networksUnderMaintenance
Expand Down
9 changes: 5 additions & 4 deletions config/example-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
"websiteDomain": "http://localhost:3000",
"wsBaseURL": "http://localhost:5000",
"showTestNetworks": false,
"grpcBCHNodeURL": "bchd.greyh.at:8335",
"grpcXECNodeURL": "grpc.fabien.cash:8335",
"chronikClientURL": "https://xec.paybutton.io",
"priceAPIURL": "https://coin.dance/api/",
"redisURL": "redis://paybutton-cache:6379",
"networkBlockchainClients": {
"ecash": "chronik",
"bitcoincash": "grpc"
"bitcoincash": "chronik"
},
"networkBlockchainURLs": {
"ecash": "https://chronik.fabien.cash",
"bitcoincash": "https://chronik.pay2stay.com/bch"
},
"networksUnderMaintenance": {
"bitcoincash": true
Expand Down
26 changes: 22 additions & 4 deletions config/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { KeyValueT } from 'constants/index'
import { KeyValueT, RESPONSE_MESSAGES } from 'constants/index'
import localConfig from '../paybutton-config.json'

export type BlockchainClientOptions = 'grpc' | 'chronik'
Expand All @@ -12,12 +12,10 @@ interface Config {

wsBaseURL: string
showTestNetworks: false
grpcBCHNodeURL: string
grpcXECNodeURL: string
chronikClientURL: string
priceAPIURL: string
redisURL: string
networkBlockchainClients: KeyValueT<BlockchainClientOptions>
networkBlockchainURLs: KeyValueT<string>
networksUnderMaintenance: KeyValueT<boolean>
triggerPOSTTimeout: number
sideshiftAffiliateId: string
Expand All @@ -29,6 +27,26 @@ const readConfig = (): Config => {
if (config.networksUnderMaintenance === undefined) {
config.networksUnderMaintenance = {}
}
if (config.networkBlockchainURLs === undefined) {
config.networkBlockchainURLs = {}
}
if (
(
config.networkBlockchainURLs.ecash === undefined ||
config.networkBlockchainURLs.ecash === ''
) &&
!config.networksUnderMaintenance.ecash
) {
throw new Error(RESPONSE_MESSAGES.MISSING_BLOCKCHAIN_CLIENT_URL_400('ecash').message)
} else if (
(
config.networkBlockchainURLs.bitcoincash === undefined ||
config.networkBlockchainURLs.bitcoincash === ''
) &&
!config.networksUnderMaintenance.bitcoincash
) {
throw new Error(RESPONSE_MESSAGES.MISSING_BLOCKCHAIN_CLIENT_URL_400('bitcoincash').message)
}
const wsURLSplit = config.wsBaseURL.split('//')
const noProtocolWsURL = wsURLSplit[wsURLSplit.length - 1]
if (process.env.NODE_ENV === 'production') {
Expand Down
13 changes: 8 additions & 5 deletions constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const RESPONSE_MESSAGES = {
INVALID_QUOTE_SLUG_400: { statusCode: 400, message: 'Invalid quote slug.' },
INVALID_TICKER_400: { statusCode: 400, message: 'Invalid ticker.' },
MISSING_BLOCKCHAIN_CLIENT_400: { statusCode: 400, message: 'There is no blockchain client chosen for this network.' },
MISSING_BLOCKCHAIN_CLIENT_URL_400: (networkSlug: string) => { return { statusCode: 400, message: `Missing client URL for network ${networkSlug}` } },
NO_BLOCKCHAIN_CLIENT_INSTANTIATED_400: { statusCode: 400, message: 'Blockchain client was not instantiated.' },
DEFAULT_WALLET_CANNOT_BE_DELETED_400: { statusCode: 400, message: 'A default wallet cannot be deleted.' },
NO_USER_PROFILE_FOUND_404: { statusCode: 404, message: 'User profile not found.' },
Expand Down Expand Up @@ -104,11 +105,17 @@ export const NETWORK_SLUGS: KeyValueT<string> = {
bchtest: 'bchtest',
bchreg: 'bchreg'
}

export const NETWORK_IDS_FROM_SLUGS: KeyValueT<number> = {
ecash: 1,
bitcoincash: 2
}

export const NETWORK_SLUGS_FROM_IDS: Record<number, string> = {
1: 'ecash',
2: 'bitcoincash'
}

// When fetching some address transactions, number of transactions to fetch at a time.
// On chronik, the max allowed is 200
export const FETCH_N = 200
Expand Down Expand Up @@ -163,11 +170,7 @@ export const NETWORK_TICKERS_FROM_ID: KeyValueT<string> = {
export const NETWORK_IDS: KeyValueT<number> = { XEC: 1, BCH: 2 }
export const QUOTE_IDS: KeyValueT<number> = { USD: 1, CAD: 2 }

export type BLOCKCHAIN_CLIENT_OPTIONS = 'grpc' | 'chronik'
export const NETWORK_BLOCKCHAIN_CLIENTS: KeyValueT<BLOCKCHAIN_CLIENT_OPTIONS> = {
ecash: 'chronik',
bitcoincash: 'grpc'
}
export type BLOCKCHAIN_CLIENT_OPTIONS = 'chronik'

export const UPSERT_TRANSACTION_PRICES_ON_DB_TIMEOUT = 45000
export const DEFAULT_TX_PAGE_SIZE = 100
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
modulePaths: ['./'],
setupFilesAfterEnv: ["./tests/setup.js", "./tests/setupMocks.ts"]
setupFilesAfterEnv: ["./tests/setup.js"]
};
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,13 @@
"bitcoinjs-lib": "^6.0.2",
"bs58": "^5.0.0",
"chart.js": "^3.8.0",
"chronik-client": "^0.28.0",
"chronik-client": "^0.28.1",
"cors": "^2.8.5",
"cross-env": "^7.0.2",
"dotenv-cli": "^5.1.0",
"ecashaddrjs": "^1.0.7",
"ecashaddrjs": "^1.5.8",
"express": "^4.17.1",
"graphql": "^16.3.0",
"grpc-bchrpc-node": "^0.15.2",
"helmet": "^4.4.1",
"lodash": "^4.17.21",
"micro": "^9.3.4",
Expand Down Expand Up @@ -86,6 +85,9 @@
"tsx": "^3.12.1",
"typescript": "^4.6.4"
},
"resolutions": {
"chronik-client/ecashaddrjs": "^1.5.8"
},
"lint-staged": {
"*.ts?(x)": [
"yarn eslint --fix"
Expand Down
24 changes: 10 additions & 14 deletions pages/admin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ interface IProps {

export default function Admin ({ user, isAdmin }: IProps): JSX.Element {
const router = useRouter()
const [subbedAddresses, setSubbedAddresses] = useState([])
const [currentAddresses, setCurrentAddresses] = useState([])
const [different, setDifferent] = useState(false)
const [ecashSubscribedAddresses, setEcashSubscribedAddresses] = useState<string[]>([])
const [bitcoincashSubscribedAddresses, setBitcoincashSubscribedAddresses] = useState<string[]>([])
const [users, setUsers] = useState<UserWithSupertokens[]>([])

useEffect(() => {
Expand All @@ -63,11 +62,10 @@ export default function Admin ({ user, isAdmin }: IProps): JSX.Element {
useEffect(() => {
void (async () => {
const ok = await (await fetch('chronikStatus')).json()
const subbedAddressesTableData = ok.registeredSubscriptions.map((value) => ({ address: value }))
const currentAddressesTableData = ok.currentSubscriptions.map((value) => ({ address: value }))
setSubbedAddresses(subbedAddressesTableData)
setCurrentAddresses(currentAddressesTableData)
setDifferent(ok?.different)
const subscribedEcashAddresses = ok.ecash.map((value: string) => ({ address: value }))
const subscribedBitcoincashAddresses = ok.bitcoincash.map((value: string) => ({ address: value }))
setEcashSubscribedAddresses(subscribedEcashAddresses)
setBitcoincashSubscribedAddresses(subscribedBitcoincashAddresses)
const ok2 = await (await fetch('/api/users')).json()
setUsers(ok2)
})()
Expand Down Expand Up @@ -101,12 +99,10 @@ export default function Admin ({ user, isAdmin }: IProps): JSX.Element {
return <>
<h2>Admin Dashboard</h2>
<div className={style.admin_ctn}>
<TableContainer columns={columns} data={subbedAddresses} ssr/>
{ different && <>
<p className={style.warning_message}> Warning!<br />The subscribed addresses registered since the beginning of the last deploy (list above) is different than the addresses being read by the chronik object instance (list below).</p>
<TableContainer columns={columns} data={currentAddresses} />
</>
}
<h3> Ecash</h3>
<TableContainer columns={columns} data={ecashSubscribedAddresses} ssr/>
<h3> Bitcoin Cash</h3>
<TableContainer columns={columns} data={bitcoincashSubscribedAddresses} ssr/>
<a
target="_blank"
rel="noopener noreferrer"
Expand Down
4 changes: 2 additions & 2 deletions pages/api/chronikStatus/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getSubbedAddresses } from 'services/chronikService'
import { getAllSubscribedAddresses } from 'services/chronikService'
import { fetchUserProfileFromId } from 'services/userService'
import { setSession } from 'utils/setSession'

Expand All @@ -11,7 +11,7 @@ export default async (req: any, res: any): Promise<void> => {
if (user.isAdmin !== true) {
throw new Error('unauthorised')
}
res.status(200).json(getSubbedAddresses())
res.status(200).json(getAllSubscribedAddresses())
} catch (err: any) {
switch (err.message) {
default:
Expand Down
4 changes: 2 additions & 2 deletions services/addressService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export async function fetchAllAddressesForNetworkId (networkId: number): Promise
})
}

export async function fetchAddressesInList (prefixedAddressList: string[]): Promise<AddressWithTransactionsAndNetwork[]> {
export async function fetchAddressesArray (prefixedAddressList: string[]): Promise<AddressWithTransactionsAndNetwork[]> {
return await prisma.address.findMany({
where: {
address: {
Expand Down Expand Up @@ -321,4 +321,4 @@ export async function fetchAddressesByPaybuttonId(paybuttonId: string): Promise<
}

return addressesIds
}
}
33 changes: 22 additions & 11 deletions services/blockchainService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { GrpcBlockchainClient } from './grpcService'
import { ChronikBlockchainClient } from './chronikService'
import { getObjectValueForAddress, getObjectValueForNetworkSlug } from '../utils/index'
import { RESPONSE_MESSAGES, KeyValueT, NETWORK_IDS, NETWORK_TICKERS } from '../constants/index'
Expand Down Expand Up @@ -52,34 +51,46 @@ export interface BlockchainClient {
subscribeAddresses: (addresses: Address[]) => Promise<void>
}

interface NetworkClients{
ecash?: ChronikBlockchainClient
bitcoincash?: ChronikBlockchainClient
}

export type Networks = 'ecash' | 'bitcoincash'

export interface NodeJsGlobalChronik extends NodeJS.Global {
chronik: ChronikBlockchainClient
chronik?: NetworkClients
}
declare const global: NodeJsGlobalChronik

function getBlockchainClient (networkSlug: string): BlockchainClient {
function getBlockchainClient (networkSlug: Networks): BlockchainClient {
if (!Object.keys(config.networkBlockchainClients).includes(networkSlug)) { throw new Error(RESPONSE_MESSAGES.MISSING_BLOCKCHAIN_CLIENT_400.message) }

switch (config.networkBlockchainClients[networkSlug]) {
case 'grpc' as BlockchainClientOptions:
return new GrpcBlockchainClient()
case 'chronik' as BlockchainClientOptions:
if (global.chronik === undefined && ChronikBlockchainClient !== undefined) {
console.log('creating chronik instance...')
global.chronik = new ChronikBlockchainClient()
if (global.chronik === undefined || global.chronik[networkSlug] === undefined) {
console.log('creating chronik client for ', networkSlug)
const newClient = new ChronikBlockchainClient(networkSlug)
if (global.chronik === undefined) {
global.chronik = {
[networkSlug]: newClient
}
} else {
global.chronik[networkSlug] = newClient
}
// Subscribe addresses & Sync lost transactions on DB upon client initialization
if (
process.env.NEXT_PHASE !== PHASE_PRODUCTION_BUILD &&
process.env.NODE_ENV !== 'test' &&
process.env.JOBS_ENV === undefined
) {
console.log('subscribing existent addresses...')
void global.chronik.subscribeInitialAddresses()
void newClient.subscribeInitialAddresses()
console.log('syncing missed transactions...')
void global.chronik.syncMissedTransactions()
void newClient.syncMissedTransactions()
}
}
return global.chronik
return global.chronik[networkSlug] as BlockchainClient
default:
throw new Error(RESPONSE_MESSAGES.NO_BLOCKCHAIN_CLIENT_INSTANTIATED_400.message)
}
Expand Down
Loading
Loading