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: add txs websockets #1053

Merged
merged 30 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0fe1ff5
feat: add solana send endpoint
NeOMakinG Sep 26, 2024
09d50c8
fix: use old yarn
NeOMakinG Sep 26, 2024
412c348
fix: review feedbacks
NeOMakinG Sep 27, 2024
c936425
fix: review feedbacks
NeOMakinG Sep 27, 2024
3336c96
feat: add estimate priority fee endpoint
NeOMakinG Sep 27, 2024
e2dfc1d
feat: add solana gas fees endpoint
NeOMakinG Sep 27, 2024
ad3e9ca
fix: fix
NeOMakinG Sep 27, 2024
bb0980f
Merge remote-tracking branch 'origin/develop' into solana-priority-fees
NeOMakinG Sep 30, 2024
6a2d68a
Merge branch 'solana-priority-fees' into solana-gast-estimate
NeOMakinG Sep 30, 2024
4f2f73f
feat: start ws
NeOMakinG Sep 30, 2024
bbe94a3
feat: websockets
NeOMakinG Oct 1, 2024
357ea67
fix: continue
NeOMakinG Oct 2, 2024
fa2832f
Merge branch 'develop' into block-ws
NeOMakinG Oct 2, 2024
20e431e
feat: continue
NeOMakinG Oct 2, 2024
1ced8e8
feat: continue
NeOMakinG Oct 2, 2024
14af221
Merge branch 'develop' into block-ws
NeOMakinG Oct 3, 2024
52059b4
feat: remove basic websocket
NeOMakinG Oct 3, 2024
a7b1253
Merge branch 'develop' into block-ws
kaladinlight Oct 3, 2024
6ef1be9
base websocket client abstract class and use types parsed transaction
kaladinlight Oct 3, 2024
878a830
fix: add sample
NeOMakinG Oct 4, 2024
869d1ed
feat: unsubscribe websockets before subscribing again
NeOMakinG Oct 7, 2024
87e2e9c
fix: review feedback
NeOMakinG Oct 7, 2024
41136b2
fix: switch back to normal websocket so we can parse transaction
NeOMakinG Oct 7, 2024
6226994
fix: add subid incrementation and parse tx
NeOMakinG Oct 7, 2024
571e6bb
Merge branch 'develop' into block-ws
NeOMakinG Oct 9, 2024
33a4238
fix: remove unwanted stuff
NeOMakinG Oct 9, 2024
37ba9ed
feat: review feedbacks
NeOMakinG Oct 9, 2024
b7c0d68
feat: review feedbacks
NeOMakinG Oct 9, 2024
8bf34a0
fix: remove duplicate epxponential delay
NeOMakinG Oct 9, 2024
37f9458
fix: try to fix ci
NeOMakinG Oct 9, 2024
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
10 changes: 5 additions & 5 deletions node/coinstacks/common/api/src/websocket.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import WebSocket from 'ws'
import { v4 } from 'uuid'
import { WebsocketClient } from '@shapeshiftoss/blockbook'
import { Logger } from '@shapeshiftoss/logger'
import { Registry } from './registry'
import { Prometheus } from './prometheus'
import { IWebsocketClient } from '@shapeshiftoss/blockbook'

export interface RequestPayload {
subscriptionId: string
Expand Down Expand Up @@ -42,7 +42,7 @@ export class ConnectionHandler {

private readonly websocket: WebSocket
private readonly registry: Registry
private readonly blockbook: WebsocketClient
private readonly blockbook: IWebsocketClient
private readonly prometheus: Prometheus
private readonly logger: Logger
private readonly routes: Record<Topics, Methods>
Expand All @@ -54,7 +54,7 @@ export class ConnectionHandler {
private constructor(
websocket: WebSocket,
registry: Registry,
blockbook: WebsocketClient,
blockbook: IWebsocketClient,
prometheus: Prometheus,
logger: Logger
) {
Expand Down Expand Up @@ -96,7 +96,7 @@ export class ConnectionHandler {
static start(
websocket: WebSocket,
registry: Registry,
blockbook: WebsocketClient,
blockbook: IWebsocketClient,
prometheus: Prometheus,
logger: Logger
): void {
Expand Down Expand Up @@ -159,7 +159,7 @@ export class ConnectionHandler {
}

this.subscriptionIds.clear()
this.blockbook.subscribeAddresses(this.registry.getAddresses())
this.blockbook?.subscribeAddresses(this.registry.getAddresses())
}

private handleSubscribeTxs(subscriptionId: string, data?: TxsTopicData): void {
Expand Down
43 changes: 41 additions & 2 deletions node/coinstacks/solana/api/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import express from 'express'
import { join } from 'path'
import swaggerUi from 'swagger-ui-express'
import { middleware, Prometheus } from '@shapeshiftoss/common-api'
import { ConnectionHandler, middleware, Prometheus, Registry, TransactionHandler } from '@shapeshiftoss/common-api'
import { Logger } from '@shapeshiftoss/logger'
import { RegisterRoutes } from './routes'
import { Server } from 'ws'
import { SolanaWebsocketClient } from './websocket'
import { Helius } from 'helius-sdk'
import { GeyserResultTransaction } from './models'

const PORT = process.env.PORT ?? 3000
const RPC_API_KEY = process.env.RPC_API_KEY
const WEBSOCKET_URL = process.env.WEBSOCKET_URL
const WEBSOCKET_API_KEY = process.env.WEBSOCKET_API_KEY
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved

export const logger = new Logger({
namespace: ['unchained', 'coinstacks', 'solana', 'api'],
level: process.env.LOG_LEVEL,
})

if (!WEBSOCKET_URL) throw new Error('WEBSOCKET_URL env var not set')
if (!WEBSOCKET_API_KEY) throw new Error('WEBSOCKET_API_KEY env var not set')
if (!RPC_API_KEY) throw new Error('RPC_API_KEY env var not set')

export const heliusSdk = new Helius(RPC_API_KEY)

const prometheus = new Prometheus({ coinstack: 'solana' })

const app = express()
Expand Down Expand Up @@ -45,4 +58,30 @@ app.get('/', async (_, res) => {

app.use(middleware.errorHandler, middleware.notFoundHandler)

app.listen(PORT, () => logger.info('Server started'))
const server = app.listen(PORT, () => logger.info('Server started'))

const transactionHandler: TransactionHandler<GeyserResultTransaction, GeyserResultTransaction> = async (geyserTx) => {
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
const addresses = geyserTx.transaction.message.accountKeys.map((key) => key.pubkey)
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved

return { addresses, tx: geyserTx }
}

const registry = new Registry({
addressFormatter: (address: string) => address,
// @TODO: Make it optional in the registry class
blockHandler: async () => {
return { txs: [] }
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
},
transactionHandler,
})

const heliusWebsocket = new SolanaWebsocketClient(WEBSOCKET_URL, {
apiKey: WEBSOCKET_API_KEY,
transactionHandler: registry.onTransaction.bind(registry),
})

const wsServer = new Server({ server })

wsServer.on('connection', (connection) => {
ConnectionHandler.start(connection, registry, heliusWebsocket, prometheus, logger)
})
11 changes: 5 additions & 6 deletions node/coinstacks/solana/api/src/controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Logger } from '@shapeshiftoss/logger'
import { validatePageSize } from '@shapeshiftoss/common-api'
import { VersionedMessage } from '@solana/web3.js'
import axios from 'axios'
import { EnrichedTransaction } from 'helius-sdk'
import { Body, Get, Path, Post, Query, Response, Route, Tags } from 'tsoa'
import {
BadRequestError,
Expand All @@ -9,11 +13,8 @@ import {
ValidationError,
handleError,
} from '../../../common/api/src' // unable to import models from a module with tsoa
import { heliusSdk } from './app'
import { Account, API, EstimateFeesBody, PriorityFees, Tx, TxHistory } from './models'
import { EnrichedTransaction, Helius } from 'helius-sdk'
import { VersionedMessage } from '@solana/web3.js'
import { validatePageSize } from '@shapeshiftoss/common-api'
import axios from 'axios'

const RPC_URL = process.env.RPC_URL
const RPC_API_KEY = process.env.RPC_API_KEY
Expand All @@ -31,8 +32,6 @@ export const logger = new Logger({
level: process.env.LOG_LEVEL,
})

const heliusSdk = new Helius(RPC_API_KEY)

const axiosNoRetry = axios.create({ timeout: 5000, params: { 'api-key': RPC_API_KEY } })

@Route('api/v1')
Expand Down
138 changes: 138 additions & 0 deletions node/coinstacks/solana/api/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,144 @@ export interface PriorityFees {
fast: number
}

// Geyser Types
export interface GeyserWebsocketResponse {
jsonrpc: string
method: string
params: GeyserParams
}

export interface GeyserParams {
subscription: number
result: GeyserResult
}

export interface GeyserResult {
transaction: GeyserResultTransaction
signature: string
slot: number
}

export interface GeyserResultTransaction {
transaction: GeyserTransactionDetails
meta: GeyserTransactionMeta
version: number
}

export interface GeyserTransactionMeta {
err: null
status: GeyserTransactionStatus
fee: number
preBalances: number[]
postBalances: number[]
innerInstructions: GeyserInnerInstruction[]
logMessages: string[]
preTokenBalances: GeyserTokenBalance[]
postTokenBalances: GeyserTokenBalance[]
rewards: unknown[]
computeUnitsConsumed: number
}

export interface GeyserInnerInstruction {
index: number
instructions: GeyserInnerInstructionDetails[]
}

export interface GeyserInnerInstructionDetails {
program?: string
programId: string
parsed?: GeyserParsedInstruction
stackHeight: number
accounts?: string[]
data?: string
}

export interface GeyserParsedInstruction {
info: GeyserParsedInstructionInfo
type: string
}

export interface GeyserParsedInstructionInfo {
extensionTypes?: string[]
mint?: string
lamports?: number
newAccount?: string
owner?: string
source?: string
space?: number
account?: string
amount?: string
authority?: string
destination?: string
}

export interface GeyserTokenBalance {
accountIndex: number
mint: string
uiTokenAmount: GeyserUITokenAmount
owner: string
programId: string
}

export interface GeyserUITokenAmount {
uiAmount: number
decimals: number
amount: string
uiAmountString: string
}

export interface GeyserTransactionStatus {
Ok: null
}

export interface GeyserTransactionDetails {
signatures: string[]
message: GeyserTransactionMessage
}

export interface GeyserTransactionMessage {
accountKeys: GeyserAccountKey[]
recentBlockhash: string
instructions: GeyserMessageInstruction[]
addressTableLookups: unknown[]
}

export interface GeyserAccountKey {
pubkey: string
writable: boolean
signer: boolean
source: GeyserAccountSource
}

export enum GeyserAccountSource {
Transaction = 'transaction',
}

export interface GeyserMessageInstruction {
programId: string
accounts?: string[]
data?: string
stackHeight: null
program?: string
parsed?: GeyserParsedMessageInstruction
}

export interface GeyserParsedMessageInstruction {
info: GeyserParsedMessageInstructionInfo
type: string
}

export interface GeyserParsedMessageInstructionInfo {
account?: string
mint?: string
source: string
systemProgram?: string
tokenProgram?: string
wallet?: string
destination?: string
lamports?: number
}

/**
* Contains the base64 encoded transaction message
*/
Expand Down
Loading