diff --git a/src/components/Wallets.test.jsx b/src/components/Wallets.test.jsx index 0e75c8c1c..6c1f8f5f6 100644 --- a/src/components/Wallets.test.jsx +++ b/src/components/Wallets.test.jsx @@ -202,7 +202,12 @@ describe('', () => { }) apiMock.postWalletUnlock.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve({ walletname: dummyWalletName, token: dummyToken }), + json: () => + Promise.resolve({ + walletname: dummyWalletName, + token: dummyToken, + refresh_token: dummyToken, + }), }) await act(async () => setup({})) @@ -223,7 +228,10 @@ describe('', () => { await waitFor(() => screen.findByText('wallets.wallet_preview.button_unlock')) }) - expect(mockStartWallet).toHaveBeenCalledWith(dummyWalletName, dummyToken) + expect(mockStartWallet).toHaveBeenCalledWith(dummyWalletName, { + token: dummyToken, + refresh_token: dummyToken, + }) expect(mockedNavigate).toHaveBeenCalledWith('/wallet') }) diff --git a/src/constants/config.ts b/src/constants/config.ts index 632e5dde9..c67165669 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -17,3 +17,5 @@ export const JM_MINIMUM_MAKERS_DEFAULT = 4 export const CJ_STATE_TAKER_RUNNING = 0 export const CJ_STATE_MAKER_RUNNING = 1 export const CJ_STATE_NONE_RUNNING = 2 + +export const JM_API_AUTH_TOKEN_EXPIRY: Milliseconds = Math.round(0.5 * 60 * 60 * 1_000) diff --git a/src/context/ServiceInfoContext.tsx b/src/context/ServiceInfoContext.tsx index ebb80f5ec..fb50031ba 100644 --- a/src/context/ServiceInfoContext.tsx +++ b/src/context/ServiceInfoContext.tsx @@ -17,8 +17,8 @@ import { CJ_STATE_TAKER_RUNNING, CJ_STATE_MAKER_RUNNING } from '../constants/con import * as Api from '../libs/JmWalletApi' -// interval in milliseconds for periodic session requests -const SESSION_REQUEST_INTERVAL = 10_000 +// interval for periodic session requests +const SESSION_REQUEST_INTERVAL: Milliseconds = 10_000 type AmountFraction = number type AmountCounterparties = number diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx index ea253ba18..a1bd47854 100644 --- a/src/context/WalletContext.tsx +++ b/src/context/WalletContext.tsx @@ -1,10 +1,11 @@ import { createContext, useEffect, useCallback, useState, useContext, PropsWithChildren, useMemo } from 'react' -import { getSession } from '../session' +import { getSession, setSession } from '../session' import * as fb from '../components/fb/utils' import * as Api from '../libs/JmWalletApi' import { WalletBalanceSummary, toBalanceSummary } from './BalanceSummary' +import { JM_API_AUTH_TOKEN_EXPIRY } from '../constants/config' export interface CurrentWallet { name: Api.WalletName @@ -276,6 +277,44 @@ const WalletProvider = ({ children }: PropsWithChildren) => { } }, [currentWallet]) + useEffect(() => { + if (!currentWallet) return + + const abortCtrl = new AbortController() + + const refreshToken = () => { + const session = getSession() + if (!session?.auth?.refresh_token) return + + Api.postToken( + { token: session.auth.token, signal: abortCtrl.signal }, + { + grant_type: 'refresh_token', + refresh_token: session.auth.refresh_token, + }, + ) + .then((res) => (res.ok ? res.json() : Api.Helper.throwError(res))) + .then((body) => { + const auth = { + token: body.token, + token_type: body.token_type, + expires_in: body.expires_in, + scope: body.scope, + refresh_token: body.refresh_token, + } + setSession({ name: currentWallet.name, auth }) + setCurrentWallet({ ...currentWallet, token: auth.token }) + }) + .catch((err) => console.error(err)) + } + + const interval = setInterval(refreshToken, JM_API_AUTH_TOKEN_EXPIRY / 4) + return () => { + clearInterval(interval) + abortCtrl.abort() + } + }, [currentWallet, setCurrentWallet]) + return ( `${window.JM.PUBLIC_PATH}/api` -export type ApiToken = string -export type WalletName = `${string}.jmdat` +type ApiToken = string +type WalletName = `${string}.jmdat` -export type Mixdepth = number -export type AmountSats = number // TODO: should be BigInt! Remove once every caller migrated to TypeScript. -export type BitcoinAddress = string +type Mixdepth = number +type AmountSats = number // TODO: should be BigInt! Remove once every caller migrated to TypeScript. +type BitcoinAddress = string type Vout = number -export type TxId = string -export type UtxoId = `${TxId}:${Vout}` +type TxId = string +type UtxoId = `${TxId}:${Vout}` // for JM versions <0.9.11 -export type SingleTokenAuthContext = { +type SingleTokenAuthContext = { token: ApiToken + refresh_token: undefined } // for JM versions >=0.9.11 -export type RefreshTokenAuthContext = SingleTokenAuthContext & { +type RefreshTokenAuthContext = { + token: ApiToken token_type: string // "bearer" expires_in: Seconds // 1800 scope: string refresh_token: ApiToken } -export type ApiAuthContext = SingleTokenAuthContext | RefreshTokenAuthContext +type ApiAuthContext = SingleTokenAuthContext | RefreshTokenAuthContext type WithWalletName = { walletName: WalletName @@ -48,7 +50,7 @@ type WithMixdepth = { type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' type YYYY = `2${Digit}${Digit}${Digit}` type MM = '01' | '02' | '03' | '04' | '05' | '06' | '07' | '08' | '09' | '10' | '11' | '12' -export type Lockdate = `${YYYY}-${MM}` +type Lockdate = `${YYYY}-${MM}` type WithLockdate = { lockdate: Lockdate } @@ -64,7 +66,7 @@ interface AuthApiRequestContext extends ApiRequestContext { token: ApiToken } -export type WalletRequestContext = AuthApiRequestContext & WithWalletName +type WalletRequestContext = AuthApiRequestContext & WithWalletName interface ApiError { message: string @@ -135,12 +137,12 @@ interface ConfigGetRequest { field: string } -export interface StartSchedulerRequest { +interface StartSchedulerRequest { destination_addresses: BitcoinAddress[] tumbler_options?: TumblerOptions } -export interface TumblerOptions { +interface TumblerOptions { restart?: boolean schedulefile?: string addrcount?: number @@ -490,7 +492,7 @@ const getRescanBlockchain = async ({ }) } -export class JmApiError extends Error { +class JmApiError extends Error { public response: Response constructor(response: Response, message: string) { @@ -526,4 +528,16 @@ export { getSchedule, getRescanBlockchain, Helper, + JmApiError, + ApiAuthContext, + StartSchedulerRequest, + WalletRequestContext, + ApiToken, + WalletName, + Lockdate, + TxId, + UtxoId, + Mixdepth, + AmountSats, + BitcoinAddress, }