|
| 1 | +import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; |
| 2 | +import { homedir } from "node:os"; |
| 3 | +import { dirname, join, resolve } from "node:path"; |
| 4 | + |
| 5 | +import { getAddress, isAddress } from "viem"; |
| 6 | + |
| 7 | +export type StoredWallet = { |
| 8 | + address: `0x${string}`; |
| 9 | + privateKey: `0x${string}`; |
| 10 | + createdAt: string; |
| 11 | +} |
| 12 | + |
| 13 | +type WalletState = { |
| 14 | + local?: StoredWallet; |
| 15 | +} |
| 16 | + |
| 17 | +export function walletFilePath(): string { |
| 18 | + if (process.env.FCF_CONFIG_HOME) return join(resolve(process.env.FCF_CONFIG_HOME), "wallet.json"); |
| 19 | + if (process.env.XDG_CONFIG_HOME) return join(resolve(process.env.XDG_CONFIG_HOME), "fcf", "wallet.json"); |
| 20 | + return join(homedir(), ".config", "fcf", "wallet.json"); |
| 21 | +} |
| 22 | + |
| 23 | +export function readLocalWallet(): StoredWallet | undefined { |
| 24 | + return readWalletState().local; |
| 25 | +} |
| 26 | + |
| 27 | +export function getLocalWallet(): StoredWallet { |
| 28 | + const wallet = readLocalWallet(); |
| 29 | + if (!wallet) throw new Error(`wallet not found at ${walletFilePath()}; run fcf wallet create`); |
| 30 | + return wallet; |
| 31 | +} |
| 32 | + |
| 33 | +export function saveLocalWallet(wallet: StoredWallet): void { |
| 34 | + const state = readWalletState(); |
| 35 | + state.local = wallet; |
| 36 | + writeWalletState(state); |
| 37 | +} |
| 38 | + |
| 39 | +function readWalletState(): WalletState { |
| 40 | + try { |
| 41 | + return parseWalletState(JSON.parse(readFileSync(walletFilePath(), "utf8"))); |
| 42 | + } catch (err: any) { |
| 43 | + if (err?.code === "ENOENT") return {}; |
| 44 | + throw err; |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +function writeWalletState(state: WalletState): void { |
| 49 | + const filePath = walletFilePath(); |
| 50 | + mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 }); |
| 51 | + |
| 52 | + const tmpPath = `${filePath}.${process.pid}.tmp`; |
| 53 | + writeFileSync(tmpPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 }); |
| 54 | + renameSync(tmpPath, filePath); |
| 55 | + chmodSync(filePath, 0o600); |
| 56 | +} |
| 57 | + |
| 58 | +function parseWalletState(value: any): WalletState { |
| 59 | + const state: WalletState = {}; |
| 60 | + if (value?.local !== undefined) state.local = parseStoredWallet(value.local); |
| 61 | + return state; |
| 62 | +} |
| 63 | + |
| 64 | +function parseStoredWallet(value: any): StoredWallet { |
| 65 | + if (!isAddress(value?.address)) throw new Error(`invalid wallet file: local.address`); |
| 66 | + if (!isPrivateKey(value?.privateKey)) throw new Error(`invalid wallet file: local.privateKey`); |
| 67 | + if (typeof value?.createdAt !== "string") throw new Error(`invalid wallet file: local.createdAt`); |
| 68 | + |
| 69 | + return { |
| 70 | + address: getAddress(value.address), |
| 71 | + privateKey: value.privateKey, |
| 72 | + createdAt: value.createdAt, |
| 73 | + }; |
| 74 | +} |
| 75 | + |
| 76 | +function isPrivateKey(value: unknown): value is `0x${string}` { |
| 77 | + return typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value); |
| 78 | +} |
0 commit comments