Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@
"@libp2p/tcp": "^11.0.17",
"@multiformats/multiaddr": "^13.0.1",
"@multiformats/multiaddr-to-uri": "^12.0.0",
"@open-wallet-standard/adapters": "^1.3.2",
"@open-wallet-standard/core": "^1.3.2",
Comment thread
SgtPooki marked this conversation as resolved.
Outdated
"@sentry/node": "^10.49.0",
"commander": "^14.0.3",
"datastore-core": "^11.0.4",
Expand Down
70 changes: 70 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/add/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export async function runAdd(options: AddOptions): Promise<AddResult | AddDryRun
// Initialize Synapse SDK
spinner.start('Initializing Synapse SDK...')

const config = parseCLIAuth(options)
const config = await parseCLIAuth(options)
if (dataSetMetadata) {
config.dataSetMetadata = dataSetMetadata
}
Expand Down
84 changes: 84 additions & 0 deletions src/core/ows/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* OpenWallet Standard (OWS) integration for filecoin-pin
*
* Resolves an OWS-managed wallet into a viem `Account` that the Synapse SDK
* can sign with directly. Private keys never leave the OWS core; this module
* just hands Synapse a signing surface (signMessage / signTransaction /
* signTypedData) backed by the OWS adapter.
*
* The adapter is loaded via dynamic `import()` because
* `@open-wallet-standard/core` is a napi-rs native binding without prebuilt
* artifacts for Windows or musl. A static import would crash CLI startup on
* those platforms even when the user never asks for OWS auth.
*
* @module core/ows
*/

import type { Account, Chain } from 'viem'

export interface OwsAccountOptions {
/** Wallet name or ID registered with the `ows` CLI / OWS core */
walletId: string
/** Target Filecoin chain (used to derive CAIP-2 chain ID) */
chain: Chain
/** Optional passphrase for keystore-encrypted wallets */
passphrase?: string
/** Optional account index within the wallet (defaults to 0) */
index?: number
/** Optional override for OWS vault path */
vaultPath?: string
}

interface OwsViemAdapter {
owsToViemAccount: (
walletNameOrId: string,
options?: { chain?: string; passphrase?: string; index?: number; vaultPath?: string }
) => Account
}

async function loadAdapter(): Promise<OwsViemAdapter> {
try {
return (await import('@open-wallet-standard/adapters/viem')) as unknown as OwsViemAdapter
} catch (err) {
const reason = err instanceof Error ? err.message : String(err)
throw new Error(
'OpenWallet Standard is not available on this platform. ' +
'@open-wallet-standard/core ships napi-rs prebuilt binaries for linux-x64-gnu, ' +
'linux-arm64-gnu, darwin-x64, and darwin-arm64 only (no Windows or musl/Alpine artifact today). ' +
'Use --private-key / PRIVATE_KEY instead, or run on a supported platform.\n' +
`Underlying load error: ${reason}`
)
}
}

const EVM_ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/

/**
* Build a viem `Account` backed by an OWS wallet.
*
* filecoin-pin signs via FEVM (Filecoin EVM), so we always request an
* `eip155:*` account from OWS. OWS wallets typically also expose a native
* Filecoin (`fil:*`, f1/f3 address) account derived from the same seed —
* that one is not used here, since synapse-sdk targets FEVM via viem.
*
* The returned account is a `LocalAccount` from viem's perspective; signing
* calls are delegated to the OWS native core, so the private key never
* materializes in the Node process.
*/
export async function getOwsAccount(options: OwsAccountOptions): Promise<Account> {
const { owsToViemAccount } = await loadAdapter()
const chainId = `eip155:${options.chain.id}`
const adapterOptions: Parameters<typeof owsToViemAccount>[1] = { chain: chainId }
if (options.passphrase != null) adapterOptions.passphrase = options.passphrase
if (options.index != null) adapterOptions.index = options.index
if (options.vaultPath != null) adapterOptions.vaultPath = options.vaultPath
const account = owsToViemAccount(options.walletId, adapterOptions)
if (!EVM_ADDRESS_REGEX.test(account.address)) {
throw new Error(
`OWS returned a non-EVM address (${account.address}) for wallet "${options.walletId}". ` +
'filecoin-pin signs via FEVM and requires an eip155 account. ' +
'Check that the wallet has an eip155:* entry in `ows wallet list`.'
)
}
return account
}
3 changes: 2 additions & 1 deletion src/core/synapse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ export async function initializeSynapse(config: SynapseSetupConfig, logger?: Log
)
}
throw new Error(
'No authentication provided. Supply a private key (--private-key / PRIVATE_KEY), ' +
'No authentication provided. Supply an OWS wallet (--wallet / OWS_WALLET_ID), ' +
'private key (--private-key / PRIVATE_KEY), ' +
'wallet address (--wallet-address / WALLET_ADDRESS), or session key (--session-key / SESSION_KEY).'
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/import/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export async function runCarImport(options: ImportOptions): Promise<ImportResult
// Initialize Synapse SDK
spinner.start('Initializing Synapse SDK...')

const config = parseCLIAuth(options)
const config = await parseCLIAuth(options)
if (dataSetMetadata) {
config.dataSetMetadata = dataSetMetadata
}
Expand Down
2 changes: 1 addition & 1 deletion src/payments/auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export async function runAutoSetup(options: PaymentSetupOptions): Promise<void>

try {
// Parse and validate authentication
const authConfig = parseCLIAuth(options)
const authConfig = await parseCLIAuth(options)

const logger = getCLILogger()
const synapse = await initializeSynapse(authConfig, logger)
Expand Down
2 changes: 1 addition & 1 deletion src/payments/deposit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function runDeposit(options: DepositOptions): Promise<void> {
spinner.start('Connecting...')
try {
// Parse and validate authentication
const authConfig = parseCLIAuth(options)
const authConfig = await parseCLIAuth(options)

const logger = getCLILogger()
const synapse = await initializeSynapse(authConfig, logger)
Expand Down
2 changes: 1 addition & 1 deletion src/payments/fund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export async function runFund(options: FundOptions): Promise<void> {
spinner.start('Connecting...')
try {
// Parse and validate authentication
const authConfig = parseCLIAuth(options)
const authConfig = await parseCLIAuth(options)

const logger = getCLILogger()
const synapse = await initializeSynapse(authConfig, logger)
Expand Down
2 changes: 1 addition & 1 deletion src/payments/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export async function runInteractiveSetup(options: PaymentSetupOptions): Promise
// Initialize Synapse
s.start('Initializing connection...')

const config = parseCLIAuth({ ...options, privateKey })
const config = await parseCLIAuth({ ...options, privateKey })
const synapse = await initializeSynapse(config)
const network = synapse.chain.name
const address = getClientAddress(synapse)
Expand Down
2 changes: 1 addition & 1 deletion src/payments/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export async function showPaymentStatus(options: StatusOptions): Promise<void> {

try {
// Parse and validate authentication
const authConfig = parseCLIAuth(options)
const authConfig = await parseCLIAuth(options)

const logger = getCLILogger()
const synapse = await initializeSynapse(authConfig, logger)
Expand Down
2 changes: 1 addition & 1 deletion src/payments/withdraw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function runWithdraw(options: WithdrawOptions): Promise<void> {
spinner.start('Connecting...')
try {
// Parse and validate authentication
const authConfig = parseCLIAuth(options)
const authConfig = await parseCLIAuth(options)

const logger = getCLILogger()
const synapse = await initializeSynapse(authConfig, logger)
Expand Down
2 changes: 1 addition & 1 deletion src/rm/remove-all-pieces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export async function runRmAllPieces(options: RmAllPiecesOptions): Promise<RmAll
try {
spinner.start('Initializing Synapse SDK...')

const authConfig = parseCLIAuth(options)
const authConfig = await parseCLIAuth(options)
const synapse = await initializeSynapse(authConfig, logger)
const network = synapse.chain.name

Expand Down
2 changes: 1 addition & 1 deletion src/rm/remove-piece.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export async function runRmPiece(options: RmPieceOptions): Promise<RmPieceResult
try {
spinner.start('Initializing Synapse SDK...')

const authConfig = parseCLIAuth(options)
const authConfig = await parseCLIAuth(options)
const synapse = await initializeSynapse(authConfig, logger)
const network = synapse.chain.name

Expand Down
49 changes: 41 additions & 8 deletions src/utils/cli-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

import type { Chain, Synapse } from '@filoz/synapse-sdk'
import { getRpcUrl, NETWORK_CHAINS, resolveDevnetConfig } from '../common/get-rpc-url.js'
import { getOwsAccount } from '../core/ows/index.js'
import type { SynapseSetupConfig } from '../core/synapse/index.js'
import { initializeSynapse } from '../core/synapse/index.js'
import { calibration, initializeSynapse } from '../core/synapse/index.js'
import { createLogger } from '../logger.js'

/**
Expand All @@ -18,6 +19,10 @@ import { createLogger } from '../logger.js'
export interface CLIAuthOptions {
/** Private key for standard authentication */
privateKey?: string | undefined
/** OpenWallet Standard wallet name or ID (signs in-process, key stays in vault) */
wallet?: string | undefined
/** Optional passphrase for an OWS-managed wallet */
walletPassphrase?: string | undefined
/** Wallet address for session key mode */
walletAddress?: string | undefined
/** Session key private key */
Expand Down Expand Up @@ -54,13 +59,21 @@ export interface CLIAuthOptions {
* @param options - CLI authentication options
* @returns Synapse setup config (validation happens in initializeSynapse)
*/
export function parseCLIAuth(options: CLIAuthOptions): SynapseSetupConfig {
export async function parseCLIAuth(options: CLIAuthOptions): Promise<SynapseSetupConfig> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuing on my precedence suggestion below, the complexity in here now deserves a bunch of tests to show it does what you expect, and document what you expect too

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preferences should be much more clear now

const network = options.network?.toLowerCase().trim()
const isDevnet = network === 'devnet'
const hasRpcUrl = options.rpcUrl != null && options.rpcUrl !== ''

// For devnet, fall back to the devnet user's private key if none provided
const privateKey = options.privateKey || (isDevnet ? resolveDevnetConfig().privateKey : undefined)
// Env vars are bound to the Commander options via .env() (see cli-options.ts),
// so read everything from `options` rather than process.env here.
const owsWalletId = options.wallet
const owsPassphrase = options.walletPassphrase

// For devnet, fall back to the devnet user's private key if none provided.
// OWS wallets take precedence over PRIVATE_KEY when explicitly supplied.
const privateKey = owsWalletId
? undefined
: options.privateKey || (isDevnet ? resolveDevnetConfig().privateKey : undefined)
const walletAddress = options.walletAddress
const sessionKey = options.sessionKey
const viewAddress = options.viewAddress
Expand All @@ -78,24 +91,44 @@ export function parseCLIAuth(options: CLIAuthOptions): SynapseSetupConfig {
chain = NETWORK_CHAINS.mainnet
}

// Build config incrementally; initializeSynapse() validates the final shape
// Resolve a single auth mode; initializeSynapse() validates the final shape.
// Precedence mirrors initializeSynapse: read-only, then session key, then an
// owner signer (OWS, then private key). View-only and session-key modes never
// use the owner account, so the OWS account (which lazily loads the native
// adapter and can fail on platforms without a prebuilt) is resolved only when
// an owner signer is actually needed.
const config: {
privateKey?: string
walletAddress?: string
sessionKey?: string
readOnly?: boolean
rpcUrl?: string
chain?: Chain
account?: Awaited<ReturnType<typeof getOwsAccount>>
} = {}

if (privateKey) config.privateKey = privateKey
if (viewAddress) {
config.walletAddress = viewAddress
config.readOnly = true
} else if (walletAddress && sessionKey) {
config.walletAddress = walletAddress
config.sessionKey = sessionKey
} else if (owsWalletId) {
Comment thread
SgtPooki marked this conversation as resolved.
Outdated
const owsOptions: Parameters<typeof getOwsAccount>[0] = {
walletId: owsWalletId,
chain: chain ?? calibration,
}
Comment thread
SgtPooki marked this conversation as resolved.
Outdated
if (owsPassphrase != null) owsOptions.passphrase = owsPassphrase
config.account = await getOwsAccount(owsOptions)
Comment thread
SgtPooki marked this conversation as resolved.
Outdated
} else if (privateKey) {
config.privateKey = privateKey
} else if (walletAddress) {
// Only one half of session-key auth supplied; pass it through so
// initializeSynapse can emit its targeted "requires both" error.
config.walletAddress = walletAddress
} else if (sessionKey) {
config.sessionKey = sessionKey
}
if (sessionKey) config.sessionKey = sessionKey
if (rpcUrl) config.rpcUrl = rpcUrl
if (chain) config.chain = chain
return config as SynapseSetupConfig
Expand Down Expand Up @@ -249,7 +282,7 @@ export function getCLILogger() {
}

export async function getCliSynapse(options: CLIAuthOptions): Promise<Synapse> {
const authConfig = parseCLIAuth(options)
const authConfig = await parseCLIAuth(options)
const logger = getCLILogger()
return initializeSynapse(authConfig, logger)
}
Loading
Loading