Skip to content

Commit 167c148

Browse files
authored
feat: allow read-only operations with wallet addr (#284)
* feat: allow read-only operations with wallet addr * chore: fix lint * fix: track authMode for synapse instance * test: mocks recognize read-only mode
1 parent f4adcec commit 167c148

File tree

8 files changed

+120
-14
lines changed

8 files changed

+120
-14
lines changed

src/core/synapse/index.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ import { getTelemetryConfig } from './telemetry-config.js'
1919
export * from './constants.js'
2020

2121
const WEBSOCKET_REGEX = /^ws(s)?:\/\//i
22+
const AUTH_MODE_SYMBOL = Symbol.for('filecoin-pin.authMode')
2223

2324
let synapseInstance: Synapse | null = null
2425
let storageInstance: StorageContext | null = null
2526
let currentProviderInfo: ProviderInfo | null = null
2627
let activeProvider: any = null // Track the provider for cleanup
28+
type AuthMode = 'standard' | 'session-key' | 'read-only' | 'signer'
2729

2830
/**
2931
* Complete application configuration interface
@@ -49,6 +51,8 @@ interface BaseSynapseConfig extends Omit<SynapseOptions, 'withCDN' | 'warmStorag
4951
rpcUrl?: string | undefined
5052
/** Optional override for WarmStorage contract address */
5153
warmStorageAddress?: string | undefined
54+
/** Optional flag for read-only auth (address-only signer) */
55+
readOnly?: boolean | undefined
5256
withCDN?: boolean | undefined
5357
/** Default metadata to apply when creating or reusing datasets */
5458
dataSetMetadata?: Record<string, string>
@@ -83,6 +87,16 @@ export interface SessionKeyConfig extends BaseSynapseConfig {
8387
sessionKey: string
8488
}
8589

90+
/**
91+
* Read-only authentication using an address-only signer
92+
*
93+
* This supports querying balances and status without signing transactions.
94+
*/
95+
export interface ReadOnlyConfig extends BaseSynapseConfig {
96+
walletAddress: string
97+
readOnly: true
98+
}
99+
86100
/**
87101
* Signer-based authentication with ethers Signer
88102
*/
@@ -100,7 +114,7 @@ export interface SignerConfig extends BaseSynapseConfig {
100114
* 2. Session Key: walletAddress + sessionKey
101115
* 3. Signer: ethers Signer instance
102116
*/
103-
export type SynapseSetupConfig = PrivateKeyConfig | SessionKeyConfig | SignerConfig
117+
export type SynapseSetupConfig = PrivateKeyConfig | SessionKeyConfig | ReadOnlyConfig | SignerConfig
104118

105119
/**
106120
* Structured service object containing the fully initialized Synapse SDK and
@@ -190,6 +204,18 @@ export function resetSynapseService(): void {
190204
activeProvider = null
191205
}
192206

207+
function setAuthMode(synapse: Synapse, mode: AuthMode): void {
208+
;(synapse as any)[AUTH_MODE_SYMBOL] = mode
209+
}
210+
211+
export function isViewOnlyMode(synapse: Synapse): boolean {
212+
try {
213+
return (synapse as any)[AUTH_MODE_SYMBOL] === 'read-only'
214+
} catch {
215+
return false
216+
}
217+
}
218+
193219
/**
194220
* Check if Synapse is using session key authentication
195221
*
@@ -203,6 +229,10 @@ export function resetSynapseService(): void {
203229
*/
204230
export function isSessionKeyMode(synapse: Synapse): boolean {
205231
try {
232+
const markedMode = (synapse as any)[AUTH_MODE_SYMBOL]
233+
if (markedMode === 'session-key') return true
234+
if (markedMode === 'read-only') return false
235+
206236
const client = synapse.getClient()
207237

208238
// The client might be wrapped in a NonceManager, check the underlying signer
@@ -231,30 +261,40 @@ function isSessionKeyConfig(config: Partial<SynapseSetupConfig>): config is Sess
231261
)
232262
}
233263

264+
function isReadOnlyConfig(config: Partial<SynapseSetupConfig>): config is ReadOnlyConfig {
265+
return config.readOnly === true && 'walletAddress' in config && config.walletAddress != null
266+
}
267+
234268
function isSignerConfig(config: Partial<SynapseSetupConfig>): config is SignerConfig {
235269
return 'signer' in config && config.signer != null
236270
}
237271

238272
/**
239273
* Validate authentication configuration
240274
*/
241-
function validateAuthConfig(config: Partial<SynapseSetupConfig>): 'standard' | 'session-key' | 'signer' {
275+
function validateAuthConfig(config: Partial<SynapseSetupConfig>): 'standard' | 'session-key' | 'read-only' | 'signer' {
242276
const hasPrivateKey = isPrivateKeyConfig(config)
243277
const hasSessionKey = isSessionKeyConfig(config)
278+
const hasReadOnly = isReadOnlyConfig(config)
244279
const hasSigner = isSignerConfig(config)
245280

246-
const authCount = [hasPrivateKey, hasSessionKey, hasSigner].filter(Boolean).length
281+
const authCount = [hasPrivateKey, hasSessionKey, hasReadOnly, hasSigner].filter(Boolean).length
247282

248283
if (authCount === 0) {
249-
throw new Error('Authentication required: provide either privateKey, walletAddress + sessionKey, or signer')
284+
throw new Error(
285+
'Authentication required: provide either privateKey, walletAddress + sessionKey, view-address, or signer'
286+
)
250287
}
251288

252289
if (authCount > 1) {
253-
throw new Error('Conflicting authentication: provide only one of privateKey, walletAddress + sessionKey, or signer')
290+
throw new Error(
291+
'Conflicting authentication: provide only one of privateKey, walletAddress + sessionKey, view-address, or signer'
292+
)
254293
}
255294

256295
if (hasPrivateKey) return 'standard'
257296
if (hasSessionKey) return 'session-key'
297+
if (hasReadOnly) return 'read-only'
258298
return 'signer'
259299
}
260300

@@ -382,6 +422,23 @@ export async function initializeSynapse(config: Partial<SynapseSetupConfig>, log
382422
signer: ownerSigner,
383423
})
384424
await setupSessionKey(synapse, sessionWallet, logger)
425+
setAuthMode(synapse, 'session-key')
426+
} else if (authMode === 'read-only') {
427+
// Read-only mode - type guard ensures walletAddress is defined
428+
if (!isReadOnlyConfig(config)) {
429+
throw new Error('Internal error: read-only mode but config type mismatch')
430+
}
431+
432+
const provider = createProvider(rpcURL)
433+
activeProvider = provider
434+
435+
const readOnlySigner = new AddressOnlySigner(config.walletAddress, provider)
436+
437+
synapse = await Synapse.create({
438+
...synapseOptions,
439+
signer: readOnlySigner,
440+
})
441+
setAuthMode(synapse, 'read-only')
385442
} else if (authMode === 'signer') {
386443
// Signer mode - type guard ensures signer is defined
387444
if (!isSignerConfig(config)) {
@@ -390,6 +447,7 @@ export async function initializeSynapse(config: Partial<SynapseSetupConfig>, log
390447

391448
synapse = await Synapse.create({ ...synapseOptions, signer: config.signer })
392449
activeProvider = synapse.getProvider()
450+
setAuthMode(synapse, 'signer')
393451
} else {
394452
// Private key mode - type guard ensures privateKey is defined
395453
if (!isPrivateKeyConfig(config)) {
@@ -398,6 +456,7 @@ export async function initializeSynapse(config: Partial<SynapseSetupConfig>, log
398456

399457
synapse = await Synapse.create({ ...synapseOptions, privateKey: config.privateKey })
400458
activeProvider = synapse.getProvider()
459+
setAuthMode(synapse, 'standard')
401460
}
402461

403462
const network = synapse.getNetwork()

src/index-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type {
3636
CreateStorageContextOptions,
3737
DatasetOptions,
3838
PrivateKeyConfig,
39+
ReadOnlyConfig,
3940
SessionKeyConfig,
4041
SignerConfig,
4142
SynapseService,

src/test/unit/add.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@ vi.mock('../../core/synapse/index.js', () => ({
3737
// Validate auth config (mirrors validateAuthConfig in actual code)
3838
const hasStandardAuth = config.privateKey != null
3939
const hasSessionKeyAuth = config.walletAddress != null && config.sessionKey != null
40+
const hasViewOnlyAuth = config.readOnly === true && config.walletAddress != null
4041

41-
if (!hasStandardAuth && !hasSessionKeyAuth) {
42-
throw new Error('Authentication required: provide either a privateKey or walletAddress + sessionKey')
42+
if (!hasStandardAuth && !hasSessionKeyAuth && !hasViewOnlyAuth) {
43+
throw new Error(
44+
'Authentication required: provide either privateKey, walletAddress + sessionKey, view-address, or signer'
45+
)
4346
}
4447

4548
return {

src/test/unit/data-set.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,12 @@ const {
8282
// Validate auth like the real initializeSynapse does
8383
const hasStandardAuth = config.privateKey != null
8484
const hasSessionKeyAuth = config.walletAddress != null && config.sessionKey != null
85+
const hasViewOnlyAuth = config.readOnly === true && config.walletAddress != null
8586

86-
if (!hasStandardAuth && !hasSessionKeyAuth) {
87-
throw new Error('Authentication required: provide either a privateKey or walletAddress + sessionKey')
87+
if (!hasStandardAuth && !hasSessionKeyAuth && !hasViewOnlyAuth) {
88+
throw new Error(
89+
'Authentication required: provide either privateKey, walletAddress + sessionKey, view-address, or signer'
90+
)
8891
}
8992

9093
return {

src/test/unit/import.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,12 @@ vi.mock('../../core/synapse/index.js', async () => {
110110
// Validate auth config (mirrors validateAuthConfig in actual code)
111111
const hasStandardAuth = config.privateKey != null
112112
const hasSessionKeyAuth = config.walletAddress != null && config.sessionKey != null
113+
const hasViewOnlyAuth = config.readOnly === true && config.walletAddress != null
113114

114-
if (!hasStandardAuth && !hasSessionKeyAuth) {
115-
throw new Error('Authentication required: provide either a privateKey or walletAddress + sessionKey')
115+
if (!hasStandardAuth && !hasSessionKeyAuth && !hasViewOnlyAuth) {
116+
throw new Error(
117+
'Authentication required: provide either privateKey, walletAddress + sessionKey, view-address, or signer'
118+
)
116119
}
117120

118121
const mockSynapse = new MockSynapse()

src/test/unit/synapse-service.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
55
import { createConfig } from '../../config.js'
66
import {
77
getSynapseService,
8+
initializeSynapse,
89
resetSynapseService,
910
type SynapseSetupConfig,
1011
setupSynapse,
@@ -79,6 +80,28 @@ describe('synapse-service', () => {
7980
)
8081
})
8182

83+
it('should initialize Synapse in read-only mode when requested', async () => {
84+
const readOnlyConfig: SynapseSetupConfig = {
85+
walletAddress: '0x0000000000000000000000000000000000000002',
86+
readOnly: true,
87+
rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1',
88+
}
89+
90+
const infoSpy = vi.spyOn(logger, 'info')
91+
92+
const synapse = await initializeSynapse(readOnlyConfig, logger)
93+
94+
expect(synapse).toBeDefined()
95+
expect(infoSpy).toHaveBeenCalledWith(
96+
expect.objectContaining({
97+
event: 'synapse.init',
98+
authMode: 'read-only',
99+
rpcUrl: readOnlyConfig.rpcUrl,
100+
}),
101+
'Initializing Synapse SDK'
102+
)
103+
})
104+
82105
it('should call provider selection callback', async () => {
83106
const callbacks: any[] = []
84107
const originalCreate = synapseSdk.Synapse.create

src/utils/cli-auth.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface CLIAuthOptions {
2323
walletAddress?: string | undefined
2424
/** Session key private key */
2525
sessionKey?: string | undefined
26+
/** View-only wallet address (no signing) */
27+
viewAddress?: string | undefined
2628
/** Filecoin network: mainnet or calibration */
2729
network?: string | undefined
2830
/** RPC endpoint URL (overrides network if specified) */
@@ -51,6 +53,7 @@ export function parseCLIAuth(options: CLIAuthOptions): Partial<SynapseSetupConfi
5153
const privateKey = options.privateKey || process.env.PRIVATE_KEY
5254
const walletAddress = options.walletAddress || process.env.WALLET_ADDRESS
5355
const sessionKey = options.sessionKey || process.env.SESSION_KEY
56+
const viewAddress = options.viewAddress || process.env.VIEW_ADDRESS
5457
const warmStorageAddress = options.warmStorageAddress || process.env.WARM_STORAGE_ADDRESS
5558

5659
const rpcUrl = getRpcUrl(options)
@@ -59,7 +62,12 @@ export function parseCLIAuth(options: CLIAuthOptions): Partial<SynapseSetupConfi
5962
const config: any = {}
6063

6164
if (privateKey) config.privateKey = privateKey
62-
if (walletAddress) config.walletAddress = walletAddress
65+
if (viewAddress) {
66+
config.walletAddress = viewAddress
67+
config.readOnly = true
68+
} else if (walletAddress) {
69+
config.walletAddress = walletAddress
70+
}
6371
if (sessionKey) config.sessionKey = sessionKey
6472
if (rpcUrl) config.rpcUrl = rpcUrl
6573
if (warmStorageAddress) config.warmStorageAddress = warmStorageAddress

src/utils/cli-options.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { type Command, Option } from 'commander'
1313
* - --private-key for standard authentication
1414
* - --wallet-address for session key authentication
1515
* - --session-key for session key authentication
16+
* - --view-address for read-only authentication (no signing, requires wallet address)
1617
* - --network for network selection (mainnet or calibration)
1718
* - --rpc-url for network configuration (overrides --network)
1819
*
@@ -28,8 +29,8 @@ import { type Command, Option } from 'commander'
2829
* .description('Do something')
2930
* .option('--my-option <value>', 'My custom option')
3031
* .action(async (options) => {
31-
* // options will include: privateKey, walletAddress, sessionKey, network, rpcUrl, myOption
32-
* const { privateKey, walletAddress, sessionKey, network, rpcUrl, myOption } = options
32+
* // options will include: privateKey, walletAddress, sessionKey, viewAddress, network, rpcUrl, myOption
33+
* const { privateKey, walletAddress, sessionKey, viewAddress, network, rpcUrl, myOption } = options
3334
* })
3435
*
3536
* // Add authentication options after the command is fully defined
@@ -41,6 +42,11 @@ export function addAuthOptions(command: Command): Command {
4142
.option('--private-key <key>', 'Private key for standard auth (can also use PRIVATE_KEY env)')
4243
.option('--wallet-address <address>', 'Wallet address for session key auth (can also use WALLET_ADDRESS env)')
4344
.option('--session-key <key>', 'Session key for session key auth (can also use SESSION_KEY env)')
45+
.addOption(
46+
new Option('--view-address <address>', 'View-only mode (no signing) for the specified wallet address').env(
47+
'VIEW_ADDRESS'
48+
)
49+
)
4450

4551
return addNetworkOptions(command)
4652
.addOption(

0 commit comments

Comments
 (0)