Skip to content

Commit

Permalink
Merge pull request #198 from blocknative/release/4.4.1
Browse files Browse the repository at this point in the history
Release 4.4.1 (master)
  • Loading branch information
lnbc1QWFyb24 committed Jun 22, 2022
2 parents e1577b9 + 96ebc0c commit 80f9a11
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 300 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ For apps that operate on multiple chains at once, you can use the Multichain SDK
Currently a transaction hash or account address can be subscribed to for all events. The `subscribe` method requires an `id`, `chainId` and a `type` and returns an `Observable`. The `Observable` that is returned is specific for events on this subscription and on completion or unsubscribing from the observable will automatically unsubscribe within the SDK. Alternatively you can listen on the global transactions `Observable` at `sdk.transactions$`.

```javascript
import { MultichainSDK } from 'bnc-sdk'
import Blocknative from 'bnc-sdk'

const blocknative = new MultichainSDK({ apiKey: '<YOUR_API_KEY>' })
const blocknative = Blocknative.multichain({ apiKey: '<YOUR_API_KEY>' })

// subscribe to address events
const addressSubscription = blocknative.subscribe({
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bnc-sdk",
"version": "4.4.0",
"version": "4.4.1",
"description": "SDK to connect to the blocknative backend via a websocket connection",
"keywords": [
"ethereum",
Expand Down
3 changes: 1 addition & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ export default [
output: [
{
format: 'cjs',
dir: 'dist/cjs/',
exports: 'named'
dir: 'dist/cjs/'
}
],
plugins: [
Expand Down
291 changes: 288 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,290 @@
import Blocknative from './sdk'
import { validateOptions } from './validation'
import SturdyWebSocket from 'sturdy-websocket'
import CryptoEs from 'crypto-es'
import transaction from './transaction'
import account from './account'
import event from './event'
import simulate from './simulate'
import unsubscribe from './unsubscribe'
import configuration from './configuration'
import { DEFAULT_RATE_LIMIT_RULES } from './defaults'
import { isLocalStorageAvailable } from './utilities'
import MultiChain from './multichain'

import {
sendMessage,
handleMessage,
processQueue,
createEventLog
} from './messages'

import {
InitializationOptions,
Ac,
TransactionHandler,
EventObject,
Tx,
Transaction,
Account,
Event,
Unsubscribe,
Simulate,
Destroy,
Configuration,
SDKError,
LimitRules,
EnhancedConfig,
MultiChainOptions
} from './types'

const DEFAULT_APP_NAME = 'unknown'
const DEFAULT_APP_VERSION = 'unknown'
const DEFAULT_SYSTEM = 'ethereum'

class SDK {
protected _storageKey: string
protected _connectionId: string | undefined
protected _dappId: string
protected _system: string
protected _networkId: number
protected _appName: string
protected _appVersion: string
protected _transactionHandlers: TransactionHandler[]
protected _socket: any
protected _connected: boolean
protected _sendMessage: (msg: EventObject) => void
protected _pingTimeout?: NodeJS.Timeout
protected _heartbeat?: () => void
protected _destroyed: boolean
protected _onerror: ((error: SDKError) => void) | undefined
protected _queuedMessages: string[]
protected _limitRules: LimitRules
protected _waitToRetry: null | Promise<void>
protected _processingQueue: boolean
protected _processQueue: () => Promise<void>

public transaction: Transaction
public account: Account
public event: Event
public simulate: Simulate
public unsubscribe: Unsubscribe
public destroy: Destroy
public configuration: Configuration
public watchedTransactions: Tx[]
public watchedAccounts: Ac[]
public configurations: Map<string, EnhancedConfig>

constructor(options: InitializationOptions) {
validateOptions(options)

const {
dappId,
system = DEFAULT_SYSTEM,
name = DEFAULT_APP_NAME,
appVersion = DEFAULT_APP_VERSION,
networkId,
transactionHandlers = [],
apiUrl,
ws,
onopen,
ondown,
onreopen,
onerror,
onclose
} = options

// override default timeout to allow for slow connections
const timeout = { connectTimeout: 10000 }

const socket = new SturdyWebSocket(
apiUrl || 'wss://api.blocknative.com/v0',
ws
? {
wsConstructor: ws,
...timeout
}
: { ...timeout }
)

socket.onopen = onOpen.bind(this, onopen)
socket.ondown = onDown.bind(this, ondown)
socket.onreopen = onReopen.bind(this, onreopen)
socket.onmessage = handleMessage.bind(this)
socket.onerror = (error: any) =>
onerror && onerror({ message: 'There was a WebSocket error', error })
socket.onclose = () => {
this._pingTimeout && clearInterval(this._pingTimeout)
onclose && onclose()
}

const storageKey = CryptoEs.SHA1(`${dappId} - ${name}`).toString()
const storedConnectionId =
isLocalStorageAvailable() && window.localStorage.getItem(storageKey)

this._storageKey = storageKey
this._connectionId = storedConnectionId || undefined
this._dappId = dappId
this._system = system
this._networkId = networkId
this._appName = name
this._appVersion = appVersion
this._transactionHandlers = transactionHandlers
this._socket = socket
this._connected = false
this._sendMessage = sendMessage.bind(this)
this._pingTimeout = undefined
this._destroyed = false
this._onerror = onerror
this._queuedMessages = []
this._limitRules = DEFAULT_RATE_LIMIT_RULES
this._waitToRetry = null
this._processingQueue = false
this._processQueue = processQueue.bind(this)

if (this._socket.ws.on) {
this._heartbeat = () => {
this._pingTimeout && clearTimeout(this._pingTimeout)

this._pingTimeout = setTimeout(() => {
// terminate connection if we haven't heard the server ping after server timeout plus conservative latency delay
// Sturdy Websocket will handle the new connection logic
this._socket.ws.terminate()
}, 30000 + 1000)
}

this._socket.ws.on('ping', () => {
this._heartbeat && this._heartbeat()
})
}

// public API
this.watchedTransactions = []
this.watchedAccounts = []
this.configurations = new Map()
this.transaction = transaction.bind(this)
this.account = account.bind(this)
this.event = event.bind(this)
this.simulate = simulate.bind(this)
this.unsubscribe = unsubscribe.bind(this)
this.configuration = configuration.bind(this)
this.destroy = () => {
this._socket.close()
this._destroyed = true

// call onclose manually here as SturdyWebSocket doesn't currently work as expected
// https://github.com/dphilipson/sturdy-websocket/issues/5
this._socket.onclose()
}
}

static multichain(options: MultiChainOptions) {
return new MultiChain(options, this)
}
}

function onOpen(this: any, handler: (() => void) | undefined) {
this._connected = true

const msg = {
categoryCode: 'initialize',
eventCode: 'checkDappId',
connectionId: this._connectionId
}

// send this message directly rather than put in queue
this._socket.send(createEventLog.bind(this)(msg))
this._heartbeat && this._heartbeat()
handler && handler()
}

function onDown(
this: any,
handler: ((closeEvent: CloseEvent) => void) | undefined,
closeEvent: CloseEvent
) {
this._connected = false

if (handler) {
handler(closeEvent)
}

this._pingTimeout && clearTimeout(this._pingTimeout)
}

async function onReopen(this: any, handler: (() => void) | undefined) {
this._connected = true

const msg = {
categoryCode: 'initialize',
eventCode: 'checkDappId',
connectionId: this._connectionId
}

this._socket.send(createEventLog.bind(this)(msg))

// re-register all configurations on re-connection
const configurations: EnhancedConfig[] = Array.from(
this.configurations.values()
)

// register global config first and wait for it to complete
const globalConfiguration = this.configurations.get('global')

if (globalConfiguration) {
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { emitter, subscription, ...config } = globalConfiguration
await this.configuration(config)
} catch (error) {
console.warn(
'Error re-sending global configuration upon reconnection:',
error
)
}
}

const addressConfigurations = configurations.filter(
({ scope }) => scope !== 'global'
)

addressConfigurations.forEach((enhancedConfig: EnhancedConfig) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { emitter, subscription, ...config } = enhancedConfig

this._sendMessage({
categoryCode: 'configs',
eventCode: 'put',
config
})
})

// re-register all accounts to be watched by server upon
// re-connection as they don't get transferred over automatically
// to the new connection like tx hashes do
this.watchedAccounts.forEach((account: Ac) => {
this._sendMessage({
eventCode: 'accountAddress',
categoryCode: 'watch',
account: {
address: account.address
}
})
})

if (handler) {
handler()
}

if (this._socket.ws && this._socket.ws.on) {
// need to re-register ping event since new connection
this._socket.ws.on('ping', () => {
this._heartbeat && this._heartbeat()
})

this._heartbeat()
}
}

export default SDK
export * from './types'
export { default as MultichainSDK } from './multichain'
export default Blocknative
export type { MultiChain }
8 changes: 5 additions & 3 deletions src/multichain/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Observable, Subject } from 'rxjs'
import Blocknative from '../sdk'
import type SDK from '../'
import subscribe from './subscribe'
import unsubscribe from './unsubscribe'

Expand All @@ -14,15 +14,16 @@ import {
class MultiChain {
public apiKey: string
public ws: WebSocket | void
public connections: Record<ChainId, Blocknative | null>
public connections: Record<ChainId, SDK | null>
public transactions$: Observable<EthereumTransactionData>
public errors$: Subject<SDKError>
public subscribe: typeof subscribe
public unsubscribe: typeof unsubscribe
public Blocknative: typeof SDK

protected onTransaction$: Subject<EthereumTransactionData>

constructor(options: MultiChainOptions) {
constructor(options: MultiChainOptions, Blocknative: typeof SDK) {
const { apiKey, ws } = options

this.apiKey = apiKey
Expand All @@ -31,6 +32,7 @@ class MultiChain {
this.onTransaction$ = new Subject()
this.transactions$ = this.onTransaction$.asObservable()
this.errors$ = new Subject()
this.Blocknative = Blocknative
this.subscribe = subscribe.bind(this)
this.unsubscribe = unsubscribe.bind(this)
}
Expand Down
8 changes: 4 additions & 4 deletions src/multichain/subscribe.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { fromEvent, Observable } from 'rxjs'
import { filter, finalize, takeWhile } from 'rxjs/operators'
import MultiChainWebSocket from '.'
import Blocknative from '../sdk'
import type SDK from '../'
import { networkName } from '../utilities'

import {
AccountSubscription,
EthereumTransactionData,
Subscription
} from '../types'
import { networkName } from '../utilities'

function subscribe(
this: MultiChainWebSocket,
Expand All @@ -21,7 +21,7 @@ function subscribe(
}

if (!this.connections[chainId]) {
this.connections[chainId] = new Blocknative({
this.connections[chainId] = new this.Blocknative({
system: 'ethereum',
networkId: parseInt(chainId, 16),
dappId: this.apiKey,
Expand All @@ -35,7 +35,7 @@ function subscribe(
})
}

const sdk = this.connections[chainId] as Blocknative
const sdk = this.connections[chainId] as SDK

if (type === 'account') {
const { filters = [], abi = [] } = subscription as AccountSubscription
Expand Down
Loading

0 comments on commit 80f9a11

Please sign in to comment.