feat(auth): add OpenWallet Standard wallet support - #458
Conversation
Wires @open-wallet-standard/adapters/viem into the existing AccountConfig path so signing can be delegated to OWS (private key never leaves the OWS core). Adds: - --wallet <id> / OWS_WALLET_ID - --wallet-passphrase / OWS_WALLET_PASSPHRASE Resolver order: --wallet (OWS) takes precedence over --private-key / PRIVATE_KEY. devnet auto-resolve still applies when no auth supplied. Refs #457
…port @open-wallet-standard/core ships napi-rs prebuilt binaries only for linux-x64-gnu, linux-arm64-gnu, darwin-x64, and darwin-arm64. A static import of @open-wallet-standard/adapters/viem triggers a require of the native binding at module evaluation time, crashing CLI startup on Windows runners (and any platform without a published prebuilt) even when the user never asks for OWS auth. Load the adapter via ESM dynamic `import()` inside getOwsAccount() so the native binding only resolves when --wallet / OWS_WALLET_ID is actually used. Surface a helpful error if the platform lacks a prebuilt. Makes getOwsAccount() and parseCLIAuth() async; updates the 12 call sites (already inside async action handlers) to await. Refs #457
OWS wallets typically expose both an eip155:* (EVM) account and a fil:* (native Filecoin) account derived from the same seed. filecoin-pin signs via FEVM through synapse-sdk and only uses the eip155 entry; the fil:* / f1 address is not used. Users seeing both in `ows wallet list` could reasonably assume the native one is the right pick. Add a defensive check that rejects any non-EVM address returned by the adapter, with an error message pointing at `ows wallet list`. Also document the FEVM-only scope in the module doc comment. Refs #457
There was a problem hiding this comment.
Pull request overview
This PR adds OpenWallet Standard (OWS) wallet authentication to filecoin-pin’s CLI auth resolver, allowing users to supply an OWS wallet ID (and optional passphrase) so signing can be performed by an OWS-managed viem Account instead of exposing raw private keys.
Changes:
- Added
--wallet/OWS_WALLET_IDand--wallet-passphrase/OWS_WALLET_PASSPHRASECLI options and wired them into the shared auth path. - Introduced
src/core/ows/index.tsto lazily load the OWS viem adapter and produce a viemAccount. - Updated CLI flows to
await parseCLIAuth()after making it async to support OWS account resolution.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/utils/cli-options.ts | Adds new Commander options for OWS wallet ID + passphrase. |
| src/utils/cli-auth.ts | Makes auth parsing async; adds OWS account resolution and precedence. |
| src/core/ows/index.ts | New OWS integration module that builds a viem Account via dynamic import. |
| src/core/synapse/index.ts | Updates missing-auth error message to mention OWS wallet auth. |
| src/add/add.ts | Awaits async auth parsing before Synapse init. |
| src/import/import.ts | Awaits async auth parsing before Synapse init. |
| src/payments/auto.ts | Awaits async auth parsing before Synapse init. |
| src/payments/deposit.ts | Awaits async auth parsing before Synapse init. |
| src/payments/fund.ts | Awaits async auth parsing before Synapse init. |
| src/payments/status.ts | Awaits async auth parsing before Synapse init. |
| src/payments/withdraw.ts | Awaits async auth parsing before Synapse init. |
| src/rm/remove-all-pieces.ts | Awaits async auth parsing before Synapse init. |
| src/rm/remove-piece.ts | Awaits async auth parsing before Synapse init. |
| package.json | Adds OWS core + adapter dependencies. |
| pnpm-lock.yaml | Locks OWS dependencies and platform-specific optional packages. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
# Conflicts: # src/payments/status.ts
When --rpc-url is set, probe the endpoint's chainId before requesting the OWS account so the eip155 hint matches the target RPC, instead of defaulting to calibration. Treat an empty OWS_WALLET_PASSPHRASE as not provided.
| * @returns Synapse setup config (validation happens in initializeSynapse) | ||
| */ | ||
| export function parseCLIAuth(options: CLIAuthOptions): SynapseSetupConfig { | ||
| export async function parseCLIAuth(options: CLIAuthOptions): Promise<SynapseSetupConfig> { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
preferences should be much more clear now
|
@SgtPooki did your testing extend beyond the |
I didn't do super thorough testing, but I have now. I fixed this in our implementation here, added unit tests, and also opened an upstream fix: github.com/open-wallet-standard/core#242 |
The OWS viem adapter serializes typed data with JSON.stringify, which throws on the bigints every synapse-sdk EIP-712 payload carries (clientDataSetId, nonce, pieceIndex, permit value/deadline). Override signTypedData to serialize the message the way the OWS core accepts: bigints as even-length hex, with EIP712Domain injected when absent. signMessage and signTransaction are left to the adapter. Validated byte-identical to viem's privateKeyToAccount, and confirmed on-chain (AddPieces, SchedulePieceRemovals) on Calibration.
Auth options merged from CLI flags and env vars could let an env var silently beat an explicit flag. Capture each flag's source (cli vs env) in a preAction hook and resolve precedence in parseCLIAuth: an explicit flag wins over env, and two modes from the same tier is a hard error. Session-key mode competes only when both halves are present, so a lone --wallet-address or --session-key no longer outranks a complete mode.
Interactive setup prompted for a private key whenever one was not passed, then handed it to parseCLIAuth. With an OWS wallet supplied that tripped the mutual-exclusion check. Skip the prompt only when a sign-capable mode is present: an OWS wallet or a complete session key. A view address and a lone session-key half cannot run setup, so they still prompt.
Bumps @open-wallet-standard/core and /adapters from 1.3.2 to 1.4.2, the release that adds trusted publishing.
4e101ba to
9b35c89
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (3)
src/utils/cli-auth.ts:187
- OWS mode precedence is determined only from the
walletoption source (const owsSource = sourceOf('wallet', ...)). IfOWS_WALLET_IDis set via env but the user passes--wallet-passphraseexplicitly to disambiguate from an envPRIVATE_KEY,parseCLIAuth()will still treat both modes as env-sourced and throw a conflict. This also contradicts the comment that a mode spanning several options should take the strongest source (like the session-key mode does). Consider trackingwalletPassphraseinAuthOptionSourcesand usingstrongest(walletSource, walletPassphraseSource)when computing the OWS candidate source.
const walletAddressSource = sourceOf('walletAddress', walletAddress)
const sessionKeySource = sourceOf('sessionKey', sessionKey)
const sessionSource =
walletAddressSource && sessionKeySource ? strongest(walletAddressSource, sessionKeySource) : undefined
if (sessionSource)
candidates.push({ mode: 'sessionKey', source: sessionSource, label: '--wallet-address/--session-key' })
const owsSource = sourceOf('wallet', owsWalletId)
if (owsSource) candidates.push({ mode: 'ows', source: owsSource, label: '--wallet/OWS_WALLET_ID' })
src/utils/cli-options.ts:20
collectAuthOptionSources()tracks provenance for mutually exclusive auth options, butAUTH_OPTION_NAMEScurrently omitswalletPassphrase. If a user relies on envOWS_WALLET_IDand provides--wallet-passphraseexplicitly (or vice versa), parseCLIAuth can't treat the OWS mode as explicitly selected, which can cause avoidable "Conflicting authentication options" errors when other env auth (e.g. PRIVATE_KEY) is also set. IncludewalletPassphrasein the collected option names (and inAuthOptionSources) so OWS mode can apply the same "strongest source wins" rule as session-key mode.
* Commander attribute names for the mutually exclusive auth flags whose
* provenance {@link parseCLIAuth} needs to resolve precedence.
*/
const AUTH_OPTION_NAMES = ['privateKey', 'wallet', 'walletAddress', 'sessionKey', 'viewAddress'] as const
src/utils/cli-auth.ts:123
- The conflict error text says "Provide exactly one signing mode", but the candidate set includes read-only mode (
--view-address), which is explicitly non-signing. This can be confusing when users hit a conflict involving view-only auth. Consider wording this as "authentication mode" (or similar) instead.
const conflict = (modes: AuthModeCandidate[], envOnly: boolean): Error => {
const labels = modes.map((c) => c.label).join(' and ')
const hint = envOnly ? ' Pass an explicit flag to disambiguate.' : ''
return new Error(`Conflicting authentication options: ${labels}. Provide exactly one signing mode.${hint}`)
}
What changed
Wires the OpenWallet Standard (openwallet.sh) viem adapter into filecoin-pin's existing
AccountConfigpath. Users can now authenticate via an OWS-managed wallet, and the private key never leaves the OWS native core.src/core/ows/index.tsbuilds a viemAccountfrom an OWS wallet ID via@open-wallet-standard/adapters/viem.--wallet <id>/OWS_WALLET_IDand--wallet-passphrase/OWS_WALLET_PASSPHRASEflags added to CLI commands using the shared auth options (add,import,payments,data-set,provider,rm). Theserverdaemon andsessioncommands still requirePRIVATE_KEY; OWS support there is follow-up work.parseCLIAuth()resolver:--wallettakes precedence over--private-key/PRIVATE_KEY. Devnet auto-resolve preserved.AccountConfigbranch ininitializeSynapse()), so no SDK plumbing changes are required.Status
Ready for review. This started as a spike and is now opened for review and collaboration. The read path is validated end-to-end on Calibration (see Verification below). Real signing against synapse-sdk still needs to be exercised before merge.
The Haven team (@Apha205, @HavenCTO) raised this on #457 and offered to validate OWS for their use case, resolve conflicts, and exercise real signing on Calibration. Reviewers are flagged so they can test against their setup.
Before merge: a funded
addupload orpayments setupagainst Calibration, to confirm the OWS-backedAccountsigns correctly end-to-end (the read path is already validated below).How to verify
npm i -g @open-wallet-standard/coreows wallet create --name fil-testecho "$YOUR_TESTNET_KEY" | ows wallet import --name fil-test --private-key --chain ethereumOWS_WALLET_ID=fil-test pnpm exec tsx src/cli.ts payments status --network calibrationPRIVATE_KEYset.Notes / risks
linux-x64-gnu,linux-arm64-gnu,darwin-x64,darwin-arm64. No Windows or musl/Alpine target on npm today. Windows and Alpine users continue to usePRIVATE_KEY.@open-wallet-standard/coreis Node-only (CJS, usesfs/child_process). Browser exports are unaffected, OWS only enters the CLI / server / Action paths.Verification
Validated end-to-end on Calibration with an OWS-managed wallet imported from an existing testnet private key:
Confirmed:
Account(no SDK plumbing changes).parseCLIAuth()resolver picked--wallet/OWS_WALLET_IDoverPRIVATE_KEY/ dotenv-loaded keys.privateKeyToAccount, so existing balances and data sets resolve unchanged.Behavior notes
getOwsAccount()always asks OWS for aneip155:<chainId>account (eip155:314mainnet,eip155:314159Calibration). OWS wallets typically also expose a native Filecoin entry (fil:mainnet → f1...) derived from the same seed, and that one is not used. When you runows wallet listyou'll see both; the0x...EVM address is the one filecoin-pin authenticates with.eip155:*account on the wallet when the requested chain isn't explicitly registered (wallet.accounts.find((a) => a.chainId.startsWith("eip155:"))). OWS derives one secp256k1 key shared across all EVM chains, so the same address resolves on Calibration even when the wallet only listseip155:1. Ourchain: eip155:<id>parameter is best-effort.getOwsAccount()defensively rejects any account whose address isn't a 40-char EVM address, in case a future adapter version returns a non-eip155 account.addupload orpayments setup) was not exercised in this run; only the read path throughSynapse.create+ balance/data-set queries.Refs #457
Closes #457