Skip to content

Commit

Permalink
feat: add transaction manager
Browse files Browse the repository at this point in the history
  • Loading branch information
fracek committed Feb 6, 2022
1 parent 3870387 commit 18172ea
Show file tree
Hide file tree
Showing 26 changed files with 577 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/dirty-candles-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@starknet-react/core': minor
---

Add transaction manager
15 changes: 11 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
"version": "0.0.0",
"license": "MIT",
"workspaces": [
"website",
"packages/*"
"packages/*",
"website"
],
"scripts": {
"build": "yarn workspaces run build",
"docs:start": "yarn workspace website run start",
"lint": "eslint packages --ext .js,.ts,.tsx",
"types:check": "tsc --skipLibCheck --noEmit",
"test": "jest",
"test": "jest --silent",
"release": "yarn build && yarn changeset publish",
"prepare": "husky install"
},
Expand Down Expand Up @@ -73,7 +73,14 @@
}
},
"rules": {
"prettier/prettier": "error"
"prettier/prettier": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
"varsIgnorePattern": "(^_)|(^tw$)",
"argsIgnorePattern": "^_"
}
]
}
},
"eslintIgnore": [
Expand Down
16 changes: 10 additions & 6 deletions packages/core/src/hooks/invoke.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useCallback, useReducer } from 'react'
import { AddTransactionResponse, Args, Contract } from 'starknet'
import { useStarknetTransactionManager } from '..'

interface State {
data?: AddTransactionResponse
data?: string
loading: boolean
error?: string
}
Expand Down Expand Up @@ -36,7 +37,7 @@ function starknetInvokeReducer(state: State, action: Action): State {
} else if (action.type === 'set_invoke_response') {
return {
...state,
data: action.data,
data: action.data.transaction_hash,
error: undefined,
loading: false,
}
Expand All @@ -63,14 +64,15 @@ interface UseStarknetInvokeArgs {
}

export interface UseStarknetInvoke {
data?: Args
data?: string
loading: boolean
error?: string
reset: () => void
invoke: (args: Args) => Promise<AddTransactionResponse | undefined>
invoke: ({ args: Args }) => Promise<AddTransactionResponse | undefined>
}

export function useStarknetInvoke({ contract, method }: UseStarknetInvokeArgs): UseStarknetInvoke {
const { addTransaction } = useStarknetTransactionManager()
const [state, dispatch] = useReducer(starknetInvokeReducer, {
loading: false,
})
Expand All @@ -80,11 +82,13 @@ export function useStarknetInvoke({ contract, method }: UseStarknetInvokeArgs):
}, [dispatch])

const invoke = useCallback(
async (args: Args) => {
async ({ args }: { args: Args }) => {
if (contract && method && args) {
try {
const response = await contract.invoke(method, args)
dispatch({ type: 'set_invoke_response', data: response })
// start tracking the transaction
addTransaction({ status: response.code, transactionHash: response.transaction_hash })
} catch (err) {
if (err.message) {
dispatch({ type: 'set_invoke_error', error: err.message })
Expand All @@ -95,7 +99,7 @@ export function useStarknetInvoke({ contract, method }: UseStarknetInvokeArgs):
}
return undefined
},
[contract, method]
[contract, method, addTransaction]
)

return { data: state.data, loading: state.loading, error: state.error, reset, invoke }
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export { StarknetBlockProvider, useStarknetBlock } from './providers/block'
export { StarknetProvider, useStarknet } from './providers/starknet'
export { useStarknetBlock } from './providers/block'
export { useStarknet } from './providers/starknet'
export { useStarknetTransactionManager } from './providers/transaction'
export type { Transaction } from './providers/transaction'
export { StarknetProvider } from './providers'
export * from './hooks'
18 changes: 18 additions & 0 deletions packages/core/src/providers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'
import { StarknetBlockProvider } from './block'
import { StarknetTransactionManagerProvider } from './transaction'
import { StarknetLibraryProvider } from './starknet'

interface StarknetProviderProps {
children?: React.ReactNode
}

export function StarknetProvider({ children }: StarknetProviderProps): JSX.Element {
return (
<StarknetLibraryProvider>
<StarknetBlockProvider>
<StarknetTransactionManagerProvider>{children}</StarknetTransactionManagerProvider>
</StarknetBlockProvider>
</StarknetLibraryProvider>
)
}
2 changes: 1 addition & 1 deletion packages/core/src/providers/starknet/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface StarknetProviderProps {
children: React.ReactNode
}

export function StarknetProvider({ children }: StarknetProviderProps): JSX.Element {
export function StarknetLibraryProvider({ children }: StarknetProviderProps): JSX.Element {
const state = useStarknetManager()
return <StarknetContext.Provider value={state}>{children}</StarknetContext.Provider>
}
11 changes: 11 additions & 0 deletions packages/core/src/providers/transaction/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext, useContext } from 'react'

import { StarknetTransactionManager, TRANSACTION_MANAGER_INITIAL_STATE } from './model'

export const TransactionManagerContext = createContext<StarknetTransactionManager>(
TRANSACTION_MANAGER_INITIAL_STATE
)

export function useStarknetTransactionManager(): StarknetTransactionManager {
return useContext(TransactionManagerContext)
}
3 changes: 3 additions & 0 deletions packages/core/src/providers/transaction/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './context'
export * from './model'
export * from './provider'
30 changes: 30 additions & 0 deletions packages/core/src/providers/transaction/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Status, TransactionStatus, Transaction as StarknetTransaction } from 'starknet'

export interface TransactionSubmitted {
status: TransactionStatus
transactionHash: string
address?: string
}

export interface TransactionReceived {
status: Status
transaction: StarknetTransaction
transactionHash: string
lastUpdatedAt: number
}

export type Transaction = TransactionSubmitted | TransactionReceived

export interface StarknetTransactionManager {
transactions: Transaction[]
addTransaction: (transaction: TransactionSubmitted) => void
removeTransaction: (transactionHash: string) => void
refreshTransaction: (transactionHash: string) => void
}

export const TRANSACTION_MANAGER_INITIAL_STATE: StarknetTransactionManager = {
transactions: [],
addTransaction: (_transaction) => undefined,
removeTransaction: (_transactionHash) => undefined,
refreshTransaction: (_transactionHash) => undefined,
}
109 changes: 109 additions & 0 deletions packages/core/src/providers/transaction/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useCallback, useEffect, useReducer } from 'react'
import { List } from 'immutable'
import { useStarknet } from '../starknet'
import { TransactionManagerContext } from './context'
import { Transaction, TransactionSubmitted } from './model'
import { transactionManagerReducer } from './reducer'

function shouldRefreshTransaction(transaction: Transaction, now: number): boolean {
// try to get transaction data as soon as possible
if (transaction.status === 'TRANSACTION_RECEIVED') {
return true
}

// wont' be updated anymore
if (transaction.status === 'ACCEPTED_ON_L1' || transaction.status === 'REJECTED') {
return false
}

// every couple of minutes is enough. Blocks finalized infrequently.
if (transaction.status === 'ACCEPTED_ON_L2') {
return now - transaction.lastUpdatedAt > 120000
}

return now - transaction.lastUpdatedAt > 15000
}

interface StarknetTransactionManagerProviderProps {
children: React.ReactNode
interval?: number
}

export function StarknetTransactionManagerProvider({
children,
interval,
}: StarknetTransactionManagerProviderProps): JSX.Element {
const { library } = useStarknet()

const [state, dispatch] = useReducer(transactionManagerReducer, {
transactions: List<Transaction>(),
})

const refresh = useCallback(
async (transactionHash: string) => {
try {
const transactionResponse = await library.getTransaction(transactionHash)
const lastUpdatedAt = Date.now()
dispatch({ type: 'update_transaction', transactionResponse, lastUpdatedAt })
} catch (err) {
// TODO(fra): somehow should track the error
console.error(err)
}
},
[library, dispatch]
)

const refreshAllTransactions = useCallback(() => {
const now = Date.now()
for (const transaction of state.transactions) {
if (shouldRefreshTransaction(transaction, now)) {
refresh(transaction.transactionHash)
}
}
}, [state.transactions, refresh])

const addTransaction = useCallback(
(transaction: TransactionSubmitted) => {
dispatch({ type: 'add_transaction', transaction })
},
[dispatch]
)

const removeTransaction = useCallback(
(transactionHash: string) => {
dispatch({ type: 'remove_transaction', transactionHash })
},
[dispatch]
)

const refreshTransaction = useCallback(
(transactionHash: string) => {
refresh(transactionHash)
},
[refresh]
)

// periodically refresh all transactions.
// do this more often than once per block since there are
// different stages of "accepted" transactions.
useEffect(() => {
refreshAllTransactions()
const intervalId = setInterval(() => {
refreshAllTransactions()
}, interval ?? 5000)
return () => clearInterval(intervalId)
}, [interval, refreshAllTransactions])

return (
<TransactionManagerContext.Provider
value={{
transactions: state.transactions.toArray(),
addTransaction,
removeTransaction,
refreshTransaction,
}}
>
{children}
</TransactionManagerContext.Provider>
)
}
72 changes: 72 additions & 0 deletions packages/core/src/providers/transaction/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { GetTransactionResponse } from 'starknet'
import { List } from 'immutable'
import { Transaction, TransactionSubmitted } from './model'

export interface TransactionManagerState {
transactions: List<Transaction>
}

interface AddTransaction {
type: 'add_transaction'
transaction: TransactionSubmitted
}

interface RemoveTransaction {
type: 'remove_transaction'
transactionHash: string
}

interface UpdateTransaction {
type: 'update_transaction'
transactionResponse: GetTransactionResponse
lastUpdatedAt: number
}

export type Action = AddTransaction | RemoveTransaction | UpdateTransaction

export function transactionManagerReducer(
state: TransactionManagerState,
action: Action
): TransactionManagerState {
if (action.type === 'add_transaction') {
return {
...state,
transactions: state.transactions.push(action.transaction),
}
} else if (action.type === 'remove_transaction') {
return {
...state,
transactions: state.transactions.filter(
(tx) => tx.transactionHash !== action.transactionHash
),
}
} else if (action.type === 'update_transaction') {
if (action.transactionResponse.status === 'NOT_RECEIVED') {
return state
}

const entry = state.transactions.findEntry(
(tx) => tx.transactionHash === action.transactionResponse.transaction['transaction_hash']
)

if (!entry) {
return state
}

const [transactionIndex, _oldTransaction] = entry

const newTransaction: Transaction = {
status: action.transactionResponse.status,
transaction: action.transactionResponse.transaction,
transactionHash: action.transactionResponse.transaction['transaction_hash'],
lastUpdatedAt: action.lastUpdatedAt,
}

return {
...state,
transactions: state.transactions.set(transactionIndex, newTransaction),
}
}

return state
}
8 changes: 2 additions & 6 deletions packages/core/test/hooks/call.test.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import React from 'react'
import { act, renderHook } from '@testing-library/react-hooks'
import { Contract } from 'starknet'
import { useStarknetCall, StarknetProvider, StarknetBlockProvider } from '../../src'
import { useStarknetCall, StarknetProvider } from '../../src'

import { CounterAbi, COUNTER_ADDRESS } from '../shared/counter'

describe('useStarknetCall', () => {
const contract = new Contract(CounterAbi, COUNTER_ADDRESS)

it('performs a call to the specified contract and method', async () => {
const wrapper = ({ children }) => (
<StarknetProvider>
<StarknetBlockProvider>{children}</StarknetBlockProvider>
</StarknetProvider>
)
const wrapper = ({ children }) => <StarknetProvider>{children}</StarknetProvider>

const { result, waitForValueToChange } = renderHook(
() => useStarknetCall({ contract, method: 'counter', args: {} }),
Expand Down
1 change: 1 addition & 0 deletions packages/core/test/hooks/contract.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('useContract', () => {
const { result } = renderHook(() => useContract({ abi: CounterAbi as Abi[], address }), {
wrapper,
})

expect(result.current).not.toBeUndefined()
expect(result.current.contract.connectedTo).toEqual(address)
})
Expand Down
Loading

0 comments on commit 18172ea

Please sign in to comment.