@wlfi-agent/cli 1.4.13 → 1.4.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +3968 -0
- package/Cargo.toml +50 -0
- package/README.md +426 -6
- package/crates/vault-cli-admin/Cargo.toml +26 -0
- package/crates/vault-cli-admin/src/io_utils.rs +500 -0
- package/crates/vault-cli-admin/src/main.rs +3990 -0
- package/crates/vault-cli-admin/src/shared_config.rs +624 -0
- package/crates/vault-cli-admin/src/tui/amounts.rs +180 -0
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +250 -0
- package/crates/vault-cli-admin/src/tui/utils.rs +82 -0
- package/crates/vault-cli-admin/src/tui.rs +3410 -0
- package/crates/vault-cli-agent/Cargo.toml +24 -0
- package/crates/vault-cli-agent/src/io_utils.rs +576 -0
- package/crates/vault-cli-agent/src/main.rs +833 -0
- package/crates/vault-cli-daemon/Cargo.toml +28 -0
- package/crates/vault-cli-daemon/src/bin/wlfi-agent-system-keychain.rs +216 -0
- package/crates/vault-cli-daemon/src/main.rs +644 -0
- package/crates/vault-cli-daemon/src/relay_sync.rs +894 -0
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +167 -0
- package/crates/vault-daemon/Cargo.toml +32 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +1041 -0
- package/crates/vault-daemon/src/daemon_parts/core_helpers.rs +1256 -0
- package/crates/vault-daemon/src/daemon_parts/types_api_rpc.rs +622 -0
- package/crates/vault-daemon/src/lib.rs +54 -0
- package/crates/vault-daemon/src/persistence.rs +441 -0
- package/crates/vault-daemon/src/tests.rs +237 -0
- package/crates/vault-daemon/src/tests_parts/part1.rs +1224 -0
- package/crates/vault-daemon/src/tests_parts/part2.rs +1021 -0
- package/crates/vault-daemon/src/tests_parts/part3.rs +835 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +604 -0
- package/crates/vault-domain/Cargo.toml +20 -0
- package/crates/vault-domain/src/action.rs +849 -0
- package/crates/vault-domain/src/address.rs +51 -0
- package/crates/vault-domain/src/approval.rs +90 -0
- package/crates/vault-domain/src/constants.rs +4 -0
- package/crates/vault-domain/src/error.rs +54 -0
- package/crates/vault-domain/src/keys.rs +71 -0
- package/crates/vault-domain/src/lib.rs +42 -0
- package/crates/vault-domain/src/nonce.rs +102 -0
- package/crates/vault-domain/src/policy.rs +172 -0
- package/crates/vault-domain/src/request.rs +53 -0
- package/crates/vault-domain/src/scope.rs +24 -0
- package/crates/vault-domain/src/session.rs +50 -0
- package/crates/vault-domain/src/signature.rs +34 -0
- package/crates/vault-domain/src/tests.rs +651 -0
- package/crates/vault-domain/src/u128_as_decimal_string.rs +44 -0
- package/crates/vault-policy/Cargo.toml +17 -0
- package/crates/vault-policy/src/engine.rs +301 -0
- package/crates/vault-policy/src/error.rs +81 -0
- package/crates/vault-policy/src/lib.rs +17 -0
- package/crates/vault-policy/src/report.rs +34 -0
- package/crates/vault-policy/src/tests.rs +891 -0
- package/crates/vault-policy/src/tests_explain.rs +78 -0
- package/crates/vault-sdk-agent/Cargo.toml +21 -0
- package/crates/vault-sdk-agent/src/lib.rs +711 -0
- package/crates/vault-signer/Cargo.toml +25 -0
- package/crates/vault-signer/src/lib.rs +731 -0
- package/crates/vault-signer/tests/secure_enclave_acl.rs +54 -0
- package/crates/vault-transport-unix/Cargo.toml +24 -0
- package/crates/vault-transport-unix/src/lib.rs +1640 -0
- package/crates/vault-transport-xpc/Cargo.toml +25 -0
- package/crates/vault-transport-xpc/src/client_codec_api.rs +635 -0
- package/crates/vault-transport-xpc/src/lib.rs +680 -0
- package/crates/vault-transport-xpc/src/tests.rs +818 -0
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +773 -0
- package/dist/cli.cjs +35088 -0
- package/dist/cli.cjs.map +1 -0
- package/package.json +49 -43
- package/packages/cache/.turbo/turbo-build.log +52 -0
- package/packages/cache/dist/chunk-2QFWMUXT.cjs +43 -0
- package/packages/cache/dist/chunk-2QFWMUXT.cjs.map +1 -0
- package/packages/cache/dist/chunk-4U63TZTQ.js +43 -0
- package/packages/cache/dist/chunk-4U63TZTQ.js.map +1 -0
- package/packages/cache/dist/chunk-ALQ6H7KG.cjs +404 -0
- package/packages/cache/dist/chunk-ALQ6H7KG.cjs.map +1 -0
- package/packages/cache/dist/chunk-FGJEEF5N.js +404 -0
- package/packages/cache/dist/chunk-FGJEEF5N.js.map +1 -0
- package/packages/cache/dist/chunk-UYNEHZHB.cjs +45 -0
- package/packages/cache/dist/chunk-UYNEHZHB.cjs.map +1 -0
- package/packages/cache/dist/chunk-VXVMPG3W.js +45 -0
- package/packages/cache/dist/chunk-VXVMPG3W.js.map +1 -0
- package/packages/cache/dist/client/index.cjs +11 -0
- package/packages/cache/dist/client/index.cjs.map +1 -0
- package/packages/cache/dist/client/index.d.cts +15 -0
- package/packages/cache/dist/client/index.d.ts +15 -0
- package/packages/cache/dist/client/index.js +11 -0
- package/packages/cache/dist/client/index.js.map +1 -0
- package/packages/cache/dist/errors/index.cjs +11 -0
- package/packages/cache/dist/errors/index.cjs.map +1 -0
- package/packages/cache/dist/errors/index.d.cts +26 -0
- package/packages/cache/dist/errors/index.d.ts +26 -0
- package/packages/cache/dist/errors/index.js +11 -0
- package/packages/cache/dist/errors/index.js.map +1 -0
- package/packages/cache/dist/index.cjs +29 -0
- package/packages/cache/dist/index.cjs.map +1 -0
- package/packages/cache/dist/index.d.cts +4 -0
- package/packages/cache/dist/index.d.ts +4 -0
- package/packages/cache/dist/index.js +29 -0
- package/packages/cache/dist/index.js.map +1 -0
- package/packages/cache/dist/service/index.cjs +15 -0
- package/packages/cache/dist/service/index.cjs.map +1 -0
- package/packages/cache/dist/service/index.d.cts +184 -0
- package/packages/cache/dist/service/index.d.ts +184 -0
- package/packages/cache/dist/service/index.js +15 -0
- package/packages/cache/dist/service/index.js.map +1 -0
- package/packages/cache/node_modules/.bin/jiti +17 -0
- package/packages/cache/node_modules/.bin/tsc +17 -0
- package/packages/cache/node_modules/.bin/tsserver +17 -0
- package/packages/cache/node_modules/.bin/tsup +17 -0
- package/packages/cache/node_modules/.bin/tsup-node +17 -0
- package/packages/cache/node_modules/.bin/tsx +17 -0
- package/packages/cache/node_modules/.bin/vitest +17 -0
- package/packages/cache/package.json +48 -0
- package/packages/cache/src/client/index.ts +56 -0
- package/packages/cache/src/errors/index.ts +53 -0
- package/packages/cache/src/index.ts +3 -0
- package/packages/cache/src/service/index.test.ts +263 -0
- package/packages/cache/src/service/index.ts +678 -0
- package/packages/cache/tsconfig.json +13 -0
- package/packages/cache/tsup.config.ts +13 -0
- package/packages/cache/vitest.config.ts +16 -0
- package/packages/config/.turbo/turbo-build.log +18 -0
- package/packages/config/dist/index.cjs +1037 -0
- package/packages/config/dist/index.cjs.map +1 -0
- package/packages/config/dist/index.d.ts +131 -0
- package/packages/config/node_modules/.bin/jiti +17 -0
- package/packages/config/node_modules/.bin/tsc +17 -0
- package/packages/config/node_modules/.bin/tsserver +17 -0
- package/packages/config/node_modules/.bin/tsup +17 -0
- package/packages/config/node_modules/.bin/tsup-node +17 -0
- package/packages/config/node_modules/.bin/tsx +17 -0
- package/packages/config/package.json +21 -0
- package/packages/config/src/index.js +1 -0
- package/packages/config/src/index.ts +1282 -0
- package/packages/config/tsconfig.json +4 -0
- package/packages/rpc/.turbo/turbo-build.log +32 -0
- package/packages/rpc/dist/_esm-BCLXDO2R.cjs +3660 -0
- package/packages/rpc/dist/_esm-BCLXDO2R.cjs.map +1 -0
- package/packages/rpc/dist/ccip-OWJLAW55.cjs +16 -0
- package/packages/rpc/dist/ccip-OWJLAW55.cjs.map +1 -0
- package/packages/rpc/dist/chunk-APQIFZ3B.cjs +6247 -0
- package/packages/rpc/dist/chunk-APQIFZ3B.cjs.map +1 -0
- package/packages/rpc/dist/chunk-CDO2GWRD.cjs +410 -0
- package/packages/rpc/dist/chunk-CDO2GWRD.cjs.map +1 -0
- package/packages/rpc/dist/chunk-QGTNTFJ7.cjs +2249 -0
- package/packages/rpc/dist/chunk-QGTNTFJ7.cjs.map +1 -0
- package/packages/rpc/dist/chunk-TZDTAHWR.cjs +44 -0
- package/packages/rpc/dist/chunk-TZDTAHWR.cjs.map +1 -0
- package/packages/rpc/dist/index.cjs +7342 -0
- package/packages/rpc/dist/index.cjs.map +1 -0
- package/packages/rpc/dist/index.d.ts +3857 -0
- package/packages/rpc/dist/secp256k1-WCNM675D.cjs +18 -0
- package/packages/rpc/dist/secp256k1-WCNM675D.cjs.map +1 -0
- package/packages/rpc/node_modules/.bin/jiti +17 -0
- package/packages/rpc/node_modules/.bin/tsc +17 -0
- package/packages/rpc/node_modules/.bin/tsserver +17 -0
- package/packages/rpc/node_modules/.bin/tsup +17 -0
- package/packages/rpc/node_modules/.bin/tsup-node +17 -0
- package/packages/rpc/node_modules/.bin/tsx +17 -0
- package/packages/rpc/package.json +25 -0
- package/packages/rpc/src/index.ts +206 -0
- package/packages/rpc/tsconfig.json +4 -0
- package/packages/typescript/base.json +36 -0
- package/packages/typescript/nextjs.json +17 -0
- package/packages/typescript/package.json +10 -0
- package/packages/ui/.turbo/turbo-build.log +44 -0
- package/packages/ui/dist/chunk-MOAFBKSA.js +11 -0
- package/packages/ui/dist/chunk-MOAFBKSA.js.map +1 -0
- package/packages/ui/dist/components/badge.d.ts +12 -0
- package/packages/ui/dist/components/badge.js +31 -0
- package/packages/ui/dist/components/badge.js.map +1 -0
- package/packages/ui/dist/components/button.d.ts +13 -0
- package/packages/ui/dist/components/button.js +40 -0
- package/packages/ui/dist/components/button.js.map +1 -0
- package/packages/ui/dist/components/card.d.ts +10 -0
- package/packages/ui/dist/components/card.js +39 -0
- package/packages/ui/dist/components/card.js.map +1 -0
- package/packages/ui/dist/components/input.d.ts +5 -0
- package/packages/ui/dist/components/input.js +28 -0
- package/packages/ui/dist/components/input.js.map +1 -0
- package/packages/ui/dist/components/label.d.ts +5 -0
- package/packages/ui/dist/components/label.js +13 -0
- package/packages/ui/dist/components/label.js.map +1 -0
- package/packages/ui/dist/components/separator.d.ts +5 -0
- package/packages/ui/dist/components/separator.js +13 -0
- package/packages/ui/dist/components/separator.js.map +1 -0
- package/packages/ui/dist/components/textarea.d.ts +5 -0
- package/packages/ui/dist/components/textarea.js +27 -0
- package/packages/ui/dist/components/textarea.js.map +1 -0
- package/packages/ui/dist/tailwind.d.ts +56 -0
- package/packages/ui/dist/tailwind.js +60 -0
- package/packages/ui/dist/tailwind.js.map +1 -0
- package/packages/ui/dist/utils/cn.d.ts +5 -0
- package/packages/ui/dist/utils/cn.js +7 -0
- package/packages/ui/dist/utils/cn.js.map +1 -0
- package/packages/ui/node_modules/.bin/jiti +17 -0
- package/packages/ui/node_modules/.bin/tsc +17 -0
- package/packages/ui/node_modules/.bin/tsserver +17 -0
- package/packages/ui/node_modules/.bin/tsup +17 -0
- package/packages/ui/node_modules/.bin/tsup-node +17 -0
- package/packages/ui/node_modules/.bin/tsx +17 -0
- package/packages/ui/package.json +69 -0
- package/packages/ui/src/components/badge.tsx +27 -0
- package/packages/ui/src/components/button.tsx +40 -0
- package/packages/ui/src/components/card.tsx +31 -0
- package/packages/ui/src/components/input.tsx +21 -0
- package/packages/ui/src/components/label.tsx +6 -0
- package/packages/ui/src/components/separator.tsx +6 -0
- package/packages/ui/src/components/textarea.tsx +20 -0
- package/packages/ui/src/globals.css +70 -0
- package/packages/ui/src/tailwind.ts +56 -0
- package/packages/ui/src/utils/cn.ts +6 -0
- package/packages/ui/tsconfig.json +20 -0
- package/packages/ui/tsup.config.ts +20 -0
- package/pnpm-workspace.yaml +4 -0
- package/scripts/install-rust-binaries.mjs +84 -0
- package/scripts/launchd/install-user-daemon.sh +358 -0
- package/scripts/launchd/run-vault-daemon.sh +5 -0
- package/scripts/launchd/run-wlfi-agent-daemon.sh +73 -0
- package/scripts/launchd/uninstall-user-daemon.sh +103 -0
- package/src/cli.ts +2121 -0
- package/src/lib/admin-guard.js +1 -0
- package/src/lib/admin-guard.ts +185 -0
- package/src/lib/admin-passthrough.ts +33 -0
- package/src/lib/admin-reset.ts +751 -0
- package/src/lib/admin-setup.ts +1612 -0
- package/src/lib/agent-auth-clear.js +1 -0
- package/src/lib/agent-auth-clear.ts +58 -0
- package/src/lib/agent-auth-forwarding.js +1 -0
- package/src/lib/agent-auth-forwarding.ts +149 -0
- package/src/lib/agent-auth-migrate.js +1 -0
- package/src/lib/agent-auth-migrate.ts +150 -0
- package/src/lib/agent-auth-revoke.ts +103 -0
- package/src/lib/agent-auth-rotate.ts +107 -0
- package/src/lib/agent-auth-token.js +1 -0
- package/src/lib/agent-auth-token.ts +25 -0
- package/src/lib/agent-auth.ts +89 -0
- package/src/lib/asset-broadcast.js +1 -0
- package/src/lib/asset-broadcast.ts +285 -0
- package/src/lib/bootstrap-artifacts.js +1 -0
- package/src/lib/bootstrap-artifacts.ts +205 -0
- package/src/lib/bootstrap-credentials.js +1 -0
- package/src/lib/bootstrap-credentials.ts +832 -0
- package/src/lib/config-amounts.js +1 -0
- package/src/lib/config-amounts.ts +189 -0
- package/src/lib/config-mutation.ts +27 -0
- package/src/lib/fs-trust.js +1 -0
- package/src/lib/fs-trust.ts +537 -0
- package/src/lib/keychain.js +1 -0
- package/src/lib/keychain.ts +225 -0
- package/src/lib/local-admin-access.ts +106 -0
- package/src/lib/network-selection.js +1 -0
- package/src/lib/network-selection.ts +71 -0
- package/src/lib/passthrough-security.js +1 -0
- package/src/lib/passthrough-security.ts +114 -0
- package/src/lib/rpc-guard.js +1 -0
- package/src/lib/rpc-guard.ts +7 -0
- package/src/lib/rust-spawn-options.js +1 -0
- package/src/lib/rust-spawn-options.ts +98 -0
- package/src/lib/rust.js +1 -0
- package/src/lib/rust.ts +143 -0
- package/src/lib/signed-tx.js +1 -0
- package/src/lib/signed-tx.ts +116 -0
- package/src/lib/status-repair-cli.ts +116 -0
- package/src/lib/sudo.js +1 -0
- package/src/lib/sudo.ts +172 -0
- package/src/lib/vault-password-forwarding.js +1 -0
- package/src/lib/vault-password-forwarding.ts +155 -0
- package/src/lib/wallet-profile.js +1 -0
- package/src/lib/wallet-profile.ts +332 -0
- package/src/lib/wallet-repair.js +1 -0
- package/src/lib/wallet-repair.ts +304 -0
- package/src/lib/wallet-setup.js +1 -0
- package/src/lib/wallet-setup.ts +1466 -0
- package/src/lib/wallet-status.js +1 -0
- package/src/lib/wallet-status.ts +640 -0
- package/tsconfig.base.json +17 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +25 -0
- package/turbo.json +41 -0
- package/LICENSE.md +0 -1
- package/dist/wlfa/index.cjs +0 -250
- package/dist/wlfa/index.d.cts +0 -1
- package/dist/wlfa/index.d.ts +0 -1
- package/dist/wlfa/index.js +0 -250
- package/dist/wlfc/index.cjs +0 -1839
- package/dist/wlfc/index.d.cts +0 -1
- package/dist/wlfc/index.d.ts +0 -1
- package/dist/wlfc/index.js +0 -1839
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export interface WlfiConfig {
|
|
6
|
+
rpcUrl?: string;
|
|
7
|
+
chainId?: number;
|
|
8
|
+
chainName?: string;
|
|
9
|
+
daemonSocket?: string;
|
|
10
|
+
stateFile?: string;
|
|
11
|
+
rustBinDir?: string;
|
|
12
|
+
agentKeyId?: string;
|
|
13
|
+
agentAuthToken?: string;
|
|
14
|
+
wallet?: WalletProfile;
|
|
15
|
+
chains?: Record<string, ChainProfile>;
|
|
16
|
+
tokens?: Record<string, TokenProfile>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WalletProfile {
|
|
20
|
+
vaultKeyId?: string;
|
|
21
|
+
vaultPublicKey: string;
|
|
22
|
+
address?: string;
|
|
23
|
+
agentKeyId?: string;
|
|
24
|
+
policyAttachment: string;
|
|
25
|
+
attachedPolicyIds?: string[];
|
|
26
|
+
policyNote?: string;
|
|
27
|
+
networkScope?: string;
|
|
28
|
+
assetScope?: string;
|
|
29
|
+
recipientScope?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ChainProfile {
|
|
33
|
+
chainId: number;
|
|
34
|
+
name: string;
|
|
35
|
+
rpcUrl?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TokenPolicyProfile {
|
|
39
|
+
perTxAmount?: number;
|
|
40
|
+
dailyAmount?: number;
|
|
41
|
+
weeklyAmount?: number;
|
|
42
|
+
perTxAmountDecimal?: string;
|
|
43
|
+
dailyAmountDecimal?: string;
|
|
44
|
+
weeklyAmountDecimal?: string;
|
|
45
|
+
perTxLimit?: string;
|
|
46
|
+
dailyLimit?: string;
|
|
47
|
+
weeklyLimit?: string;
|
|
48
|
+
maxGasPerChainWei?: string;
|
|
49
|
+
dailyMaxTxCount?: string;
|
|
50
|
+
perTxMaxFeePerGasGwei?: string;
|
|
51
|
+
perTxMaxFeePerGasWei?: string;
|
|
52
|
+
perTxMaxPriorityFeePerGasWei?: string;
|
|
53
|
+
perTxMaxCalldataBytes?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TokenDestinationOverrideProfile {
|
|
57
|
+
recipient: string;
|
|
58
|
+
limits: TokenPolicyProfile;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TokenManualApprovalProfile {
|
|
62
|
+
priority?: number;
|
|
63
|
+
recipient?: string;
|
|
64
|
+
minAmount?: number;
|
|
65
|
+
maxAmount?: number;
|
|
66
|
+
minAmountDecimal?: string;
|
|
67
|
+
maxAmountDecimal?: string;
|
|
68
|
+
minAmountWei?: string;
|
|
69
|
+
maxAmountWei?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface TokenChainProfile {
|
|
73
|
+
chainId: number;
|
|
74
|
+
isNative: boolean;
|
|
75
|
+
address?: string;
|
|
76
|
+
decimals: number;
|
|
77
|
+
defaultPolicy?: TokenPolicyProfile;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface TokenProfile {
|
|
81
|
+
name?: string;
|
|
82
|
+
symbol: string;
|
|
83
|
+
defaultPolicy?: TokenPolicyProfile;
|
|
84
|
+
destinationOverrides?: TokenDestinationOverrideProfile[];
|
|
85
|
+
manualApprovalPolicies?: TokenManualApprovalProfile[];
|
|
86
|
+
chains: Record<string, TokenChainProfile>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface TokenChainProfileEntry extends TokenChainProfile {
|
|
90
|
+
key: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TokenProfileEntry {
|
|
94
|
+
key: string;
|
|
95
|
+
name?: string;
|
|
96
|
+
symbol: string;
|
|
97
|
+
chains: TokenChainProfileEntry[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const WLFI_DIRNAME = '.wlfi_agent';
|
|
101
|
+
export const CONFIG_FILENAME = 'config.json';
|
|
102
|
+
|
|
103
|
+
const PRIVATE_DIR_MODE = 0o700;
|
|
104
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
105
|
+
const GROUP_OTHER_WRITE_MODE_MASK = 0o022;
|
|
106
|
+
const PRIVATE_FILE_MODE_MASK = 0o077;
|
|
107
|
+
const STICKY_BIT_MODE = 0o1000;
|
|
108
|
+
const MAX_CONFIG_FILE_BYTES = 256 * 1024;
|
|
109
|
+
const DEFAULT_ETH_RPC_URL = 'https://eth.llamarpc.com';
|
|
110
|
+
const DEFAULT_BSC_RPC_URL = 'https://bsc.drpc.org';
|
|
111
|
+
const DEFAULT_USD1_ADDRESS = '0xc83DE66ebA6a91B6F3d167f2ee9F0C42aD70B611';
|
|
112
|
+
|
|
113
|
+
export const BUILTIN_CHAINS: Record<string, ChainProfile> = {
|
|
114
|
+
eth: { chainId: 1, name: 'eth', rpcUrl: DEFAULT_ETH_RPC_URL },
|
|
115
|
+
ethereum: { chainId: 1, name: 'ethereum', rpcUrl: DEFAULT_ETH_RPC_URL },
|
|
116
|
+
mainnet: { chainId: 1, name: 'mainnet', rpcUrl: DEFAULT_ETH_RPC_URL },
|
|
117
|
+
sepolia: { chainId: 11155111, name: 'sepolia' },
|
|
118
|
+
base: { chainId: 8453, name: 'base' },
|
|
119
|
+
'base-sepolia': { chainId: 84532, name: 'base-sepolia' },
|
|
120
|
+
optimism: { chainId: 10, name: 'optimism' },
|
|
121
|
+
arbitrum: { chainId: 42161, name: 'arbitrum' },
|
|
122
|
+
polygon: { chainId: 137, name: 'polygon' },
|
|
123
|
+
bsc: { chainId: 56, name: 'bsc', rpcUrl: DEFAULT_BSC_RPC_URL }
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const BUILTIN_TOKENS: Record<string, TokenProfile> = {
|
|
127
|
+
bnb: {
|
|
128
|
+
name: 'BNB',
|
|
129
|
+
symbol: 'BNB',
|
|
130
|
+
defaultPolicy: defaultTokenPolicy('0.01', '0.2', '1.4'),
|
|
131
|
+
chains: {
|
|
132
|
+
bsc: {
|
|
133
|
+
chainId: 56,
|
|
134
|
+
isNative: true,
|
|
135
|
+
decimals: 18,
|
|
136
|
+
defaultPolicy: defaultTokenPolicy('0.01', '0.2', '1.4'),
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
eth: {
|
|
141
|
+
symbol: 'ETH',
|
|
142
|
+
chains: {
|
|
143
|
+
ethereum: { chainId: 1, isNative: true, decimals: 18 },
|
|
144
|
+
sepolia: { chainId: 11155111, isNative: true, decimals: 18 },
|
|
145
|
+
base: { chainId: 8453, isNative: true, decimals: 18 },
|
|
146
|
+
'base-sepolia': { chainId: 84532, isNative: true, decimals: 18 },
|
|
147
|
+
optimism: { chainId: 10, isNative: true, decimals: 18 },
|
|
148
|
+
arbitrum: { chainId: 42161, isNative: true, decimals: 18 },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
usdc: {
|
|
152
|
+
symbol: 'USDC',
|
|
153
|
+
chains: {
|
|
154
|
+
ethereum: {
|
|
155
|
+
chainId: 1,
|
|
156
|
+
isNative: false,
|
|
157
|
+
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
158
|
+
decimals: 6,
|
|
159
|
+
},
|
|
160
|
+
base: {
|
|
161
|
+
chainId: 8453,
|
|
162
|
+
isNative: false,
|
|
163
|
+
address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
164
|
+
decimals: 6,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
usd1: {
|
|
169
|
+
name: 'USD1',
|
|
170
|
+
symbol: 'USD1',
|
|
171
|
+
defaultPolicy: defaultTokenPolicy('10', '100', '700'),
|
|
172
|
+
chains: {
|
|
173
|
+
eth: {
|
|
174
|
+
chainId: 1,
|
|
175
|
+
isNative: false,
|
|
176
|
+
address: DEFAULT_USD1_ADDRESS,
|
|
177
|
+
decimals: 18,
|
|
178
|
+
defaultPolicy: defaultTokenPolicy('10', '100', '700'),
|
|
179
|
+
},
|
|
180
|
+
bsc: {
|
|
181
|
+
chainId: 56,
|
|
182
|
+
isNative: false,
|
|
183
|
+
address: DEFAULT_USD1_ADDRESS,
|
|
184
|
+
decimals: 18,
|
|
185
|
+
defaultPolicy: defaultTokenPolicy('10', '100', '700'),
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function defaultTokenPolicy(perTxAmountDecimal: string, dailyAmountDecimal: string, weeklyAmountDecimal: string): TokenPolicyProfile {
|
|
192
|
+
return {
|
|
193
|
+
perTxAmountDecimal,
|
|
194
|
+
dailyAmountDecimal,
|
|
195
|
+
weeklyAmountDecimal,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function defaultChainProfiles(): Record<string, ChainProfile> {
|
|
200
|
+
return {
|
|
201
|
+
eth: {
|
|
202
|
+
chainId: 1,
|
|
203
|
+
name: 'ETH',
|
|
204
|
+
rpcUrl: DEFAULT_ETH_RPC_URL,
|
|
205
|
+
},
|
|
206
|
+
bsc: {
|
|
207
|
+
chainId: 56,
|
|
208
|
+
name: 'BSC',
|
|
209
|
+
rpcUrl: DEFAULT_BSC_RPC_URL,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function defaultTokenProfiles(): Record<string, TokenProfile> {
|
|
215
|
+
return {
|
|
216
|
+
usd1: {
|
|
217
|
+
name: 'USD1',
|
|
218
|
+
symbol: 'USD1',
|
|
219
|
+
defaultPolicy: defaultTokenPolicy('10', '100', '700'),
|
|
220
|
+
destinationOverrides: [],
|
|
221
|
+
manualApprovalPolicies: [],
|
|
222
|
+
chains: {
|
|
223
|
+
eth: {
|
|
224
|
+
chainId: 1,
|
|
225
|
+
isNative: false,
|
|
226
|
+
address: DEFAULT_USD1_ADDRESS,
|
|
227
|
+
decimals: 18,
|
|
228
|
+
defaultPolicy: defaultTokenPolicy('10', '100', '700'),
|
|
229
|
+
},
|
|
230
|
+
bsc: {
|
|
231
|
+
chainId: 56,
|
|
232
|
+
isNative: false,
|
|
233
|
+
address: DEFAULT_USD1_ADDRESS,
|
|
234
|
+
decimals: 18,
|
|
235
|
+
defaultPolicy: defaultTokenPolicy('10', '100', '700'),
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
bnb: {
|
|
240
|
+
name: 'BNB',
|
|
241
|
+
symbol: 'BNB',
|
|
242
|
+
defaultPolicy: defaultTokenPolicy('0.01', '0.2', '1.4'),
|
|
243
|
+
destinationOverrides: [],
|
|
244
|
+
manualApprovalPolicies: [],
|
|
245
|
+
chains: {
|
|
246
|
+
bsc: {
|
|
247
|
+
chainId: 56,
|
|
248
|
+
isNative: true,
|
|
249
|
+
decimals: 18,
|
|
250
|
+
defaultPolicy: defaultTokenPolicy('0.01', '0.2', '1.4'),
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
function normalizeLoopbackHostname(hostname: string): string {
|
|
259
|
+
if (hostname.startsWith('[') && hostname.endsWith(']')) {
|
|
260
|
+
return hostname.slice(1, -1).toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return hostname.toLowerCase();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isIpv4Loopback(hostname: string): boolean {
|
|
267
|
+
const parts = hostname.split('.');
|
|
268
|
+
if (parts.length !== 4 || parts.some((part) => !/^\d+$/u.test(part))) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const octets = parts.map((part) => Number(part));
|
|
273
|
+
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return octets[0] === 127;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isLoopbackHostname(hostname: string): boolean {
|
|
281
|
+
const normalized = normalizeLoopbackHostname(hostname);
|
|
282
|
+
return normalized === 'localhost'
|
|
283
|
+
|| normalized.endsWith('.localhost')
|
|
284
|
+
|| normalized === '::1'
|
|
285
|
+
|| isIpv4Loopback(normalized);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function assertSafeRpcUrl(value: string, label = 'rpcUrl'): string {
|
|
289
|
+
const normalized = value.trim();
|
|
290
|
+
if (!normalized) {
|
|
291
|
+
throw new Error(`${label} is required`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let parsed: URL;
|
|
295
|
+
try {
|
|
296
|
+
parsed = new URL(normalized);
|
|
297
|
+
} catch {
|
|
298
|
+
throw new Error(`${label} must be a valid http(s) URL`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
302
|
+
throw new Error(`${label} must use https or localhost http`);
|
|
303
|
+
}
|
|
304
|
+
if (parsed.username || parsed.password) {
|
|
305
|
+
throw new Error(`${label} must not include embedded credentials`);
|
|
306
|
+
}
|
|
307
|
+
if (!parsed.hostname) {
|
|
308
|
+
throw new Error(`${label} must include a hostname`);
|
|
309
|
+
}
|
|
310
|
+
if (parsed.protocol === 'http:' && !isLoopbackHostname(parsed.hostname)) {
|
|
311
|
+
throw new Error(`${label} must use https unless it targets localhost or a loopback address`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return normalized;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const ADDRESS_PATTERN = /^0x[a-f0-9]{40}$/iu;
|
|
318
|
+
|
|
319
|
+
function assertValidEvmAddress(value: string, label: string): string {
|
|
320
|
+
const normalized = value.trim();
|
|
321
|
+
if (!ADDRESS_PATTERN.test(normalized)) {
|
|
322
|
+
throw new Error(`${label} must be a valid EVM address`);
|
|
323
|
+
}
|
|
324
|
+
return normalized;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function assertPositiveSafeInteger(value: number, label: string): number {
|
|
328
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
329
|
+
throw new Error(`${label} must be a positive safe integer`);
|
|
330
|
+
}
|
|
331
|
+
return value;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function assertTokenDecimals(value: number, label: string): number {
|
|
335
|
+
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
336
|
+
throw new Error(`${label} must be an integer between 0 and 255`);
|
|
337
|
+
}
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function assertOptionalTokenAmount(value: number | null | undefined, label: string): number | undefined {
|
|
342
|
+
if (value == null) {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
346
|
+
throw new Error(`${label} must be a positive finite number`);
|
|
347
|
+
}
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function assertOptionalTrimmedString(value: string | null | undefined, label: string): string | undefined {
|
|
352
|
+
if (value == null) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
const normalized = value.trim();
|
|
356
|
+
if (!normalized) {
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
return normalized;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function assertRequiredTrimmedString(value: string | null | undefined, label: string): string {
|
|
363
|
+
const normalized = assertOptionalTrimmedString(value, label);
|
|
364
|
+
if (!normalized) {
|
|
365
|
+
throw new Error(`${label} is required`);
|
|
366
|
+
}
|
|
367
|
+
return normalized;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function assertOptionalStringArray(value: string[] | null | undefined, label: string): string[] | undefined {
|
|
371
|
+
if (value == null) {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
375
|
+
throw new Error(`${label} must be an array of strings`);
|
|
376
|
+
}
|
|
377
|
+
const normalized = value
|
|
378
|
+
.map((entry) => entry.trim())
|
|
379
|
+
.filter((entry) => entry.length > 0);
|
|
380
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function normalizeWalletProfile(profile: WalletProfile | null | undefined): WalletProfile | undefined {
|
|
384
|
+
if (profile == null) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
vaultKeyId: assertOptionalTrimmedString(profile.vaultKeyId, 'wallet.vaultKeyId'),
|
|
390
|
+
vaultPublicKey: assertRequiredTrimmedString(profile.vaultPublicKey, 'wallet.vaultPublicKey'),
|
|
391
|
+
address: profile.address
|
|
392
|
+
? assertValidEvmAddress(profile.address, 'wallet.address')
|
|
393
|
+
: undefined,
|
|
394
|
+
agentKeyId: assertOptionalTrimmedString(profile.agentKeyId, 'wallet.agentKeyId'),
|
|
395
|
+
policyAttachment: assertRequiredTrimmedString(profile.policyAttachment, 'wallet.policyAttachment'),
|
|
396
|
+
attachedPolicyIds: assertOptionalStringArray(profile.attachedPolicyIds, 'wallet.attachedPolicyIds'),
|
|
397
|
+
policyNote: assertOptionalTrimmedString(profile.policyNote, 'wallet.policyNote'),
|
|
398
|
+
networkScope: assertOptionalTrimmedString(profile.networkScope, 'wallet.networkScope'),
|
|
399
|
+
assetScope: assertOptionalTrimmedString(profile.assetScope, 'wallet.assetScope'),
|
|
400
|
+
recipientScope: assertOptionalTrimmedString(profile.recipientScope, 'wallet.recipientScope'),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function normalizeTokenPolicyProfile(tokenKey: string, chainKey: string, policy: TokenPolicyProfile | undefined) {
|
|
405
|
+
if (!policy) {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
perTxAmount: assertOptionalTokenAmount(policy.perTxAmount, `token '${tokenKey}' chain '${chainKey}' perTxAmount`),
|
|
410
|
+
dailyAmount: assertOptionalTokenAmount(policy.dailyAmount, `token '${tokenKey}' chain '${chainKey}' dailyAmount`),
|
|
411
|
+
weeklyAmount: assertOptionalTokenAmount(policy.weeklyAmount, `token '${tokenKey}' chain '${chainKey}' weeklyAmount`),
|
|
412
|
+
perTxAmountDecimal: assertOptionalTrimmedString(policy.perTxAmountDecimal, `token '${tokenKey}' chain '${chainKey}' perTxAmountDecimal`),
|
|
413
|
+
dailyAmountDecimal: assertOptionalTrimmedString(policy.dailyAmountDecimal, `token '${tokenKey}' chain '${chainKey}' dailyAmountDecimal`),
|
|
414
|
+
weeklyAmountDecimal: assertOptionalTrimmedString(policy.weeklyAmountDecimal, `token '${tokenKey}' chain '${chainKey}' weeklyAmountDecimal`),
|
|
415
|
+
perTxLimit: assertOptionalTrimmedString(policy.perTxLimit, `token '${tokenKey}' chain '${chainKey}' perTxLimit`),
|
|
416
|
+
dailyLimit: assertOptionalTrimmedString(policy.dailyLimit, `token '${tokenKey}' chain '${chainKey}' dailyLimit`),
|
|
417
|
+
weeklyLimit: assertOptionalTrimmedString(policy.weeklyLimit, `token '${tokenKey}' chain '${chainKey}' weeklyLimit`),
|
|
418
|
+
maxGasPerChainWei: assertOptionalTrimmedString(policy.maxGasPerChainWei, `token '${tokenKey}' chain '${chainKey}' maxGasPerChainWei`),
|
|
419
|
+
dailyMaxTxCount: assertOptionalTrimmedString(policy.dailyMaxTxCount, `token '${tokenKey}' chain '${chainKey}' dailyMaxTxCount`),
|
|
420
|
+
perTxMaxFeePerGasGwei: assertOptionalTrimmedString(policy.perTxMaxFeePerGasGwei, `token '${tokenKey}' chain '${chainKey}' perTxMaxFeePerGasGwei`),
|
|
421
|
+
perTxMaxFeePerGasWei: assertOptionalTrimmedString(policy.perTxMaxFeePerGasWei, `token '${tokenKey}' chain '${chainKey}' perTxMaxFeePerGasWei`),
|
|
422
|
+
perTxMaxPriorityFeePerGasWei: assertOptionalTrimmedString(policy.perTxMaxPriorityFeePerGasWei, `token '${tokenKey}' chain '${chainKey}' perTxMaxPriorityFeePerGasWei`),
|
|
423
|
+
perTxMaxCalldataBytes: assertOptionalTrimmedString(policy.perTxMaxCalldataBytes, `token '${tokenKey}' chain '${chainKey}' perTxMaxCalldataBytes`),
|
|
424
|
+
} satisfies TokenPolicyProfile;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function normalizeTokenDestinationOverrideProfile(
|
|
428
|
+
tokenKey: string,
|
|
429
|
+
profile: TokenDestinationOverrideProfile
|
|
430
|
+
): TokenDestinationOverrideProfile {
|
|
431
|
+
const recipient = assertValidEvmAddress(profile.recipient, `token '${tokenKey}' destination override recipient`);
|
|
432
|
+
return {
|
|
433
|
+
recipient,
|
|
434
|
+
limits: normalizeTokenPolicyProfile(tokenKey, 'override', profile.limits) ?? {},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function normalizeTokenManualApprovalProfile(
|
|
439
|
+
tokenKey: string,
|
|
440
|
+
profile: TokenManualApprovalProfile
|
|
441
|
+
): TokenManualApprovalProfile {
|
|
442
|
+
return {
|
|
443
|
+
priority: profile.priority === undefined ? undefined : assertPositiveSafeInteger(profile.priority, `token '${tokenKey}' manual approval priority`),
|
|
444
|
+
recipient: profile.recipient
|
|
445
|
+
? assertValidEvmAddress(profile.recipient, `token '${tokenKey}' manual approval recipient`)
|
|
446
|
+
: undefined,
|
|
447
|
+
minAmount: assertOptionalTokenAmount(profile.minAmount, `token '${tokenKey}' manual approval minAmount`),
|
|
448
|
+
maxAmount: assertOptionalTokenAmount(profile.maxAmount, `token '${tokenKey}' manual approval maxAmount`),
|
|
449
|
+
minAmountDecimal: assertOptionalTrimmedString(profile.minAmountDecimal, `token '${tokenKey}' manual approval minAmountDecimal`),
|
|
450
|
+
maxAmountDecimal: assertOptionalTrimmedString(profile.maxAmountDecimal, `token '${tokenKey}' manual approval maxAmountDecimal`),
|
|
451
|
+
minAmountWei: assertOptionalTrimmedString(profile.minAmountWei, `token '${tokenKey}' manual approval minAmountWei`),
|
|
452
|
+
maxAmountWei: assertOptionalTrimmedString(profile.maxAmountWei, `token '${tokenKey}' manual approval maxAmountWei`),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function normalizeTokenChainProfile(tokenKey: string, chainKey: string, profile: TokenChainProfile): TokenChainProfile {
|
|
457
|
+
const normalizedChainKey = chainKey.trim().toLowerCase();
|
|
458
|
+
if (!normalizedChainKey) {
|
|
459
|
+
throw new Error(`token '${tokenKey}' chain key is required`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const normalized: TokenChainProfile = {
|
|
463
|
+
chainId: assertPositiveSafeInteger(profile.chainId, `token '${tokenKey}' chain '${normalizedChainKey}' chainId`),
|
|
464
|
+
isNative: Boolean(profile.isNative),
|
|
465
|
+
decimals: assertTokenDecimals(profile.decimals, `token '${tokenKey}' chain '${normalizedChainKey}' decimals`),
|
|
466
|
+
defaultPolicy: normalizeTokenPolicyProfile(tokenKey, normalizedChainKey, profile.defaultPolicy),
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
if (normalized.isNative) {
|
|
470
|
+
if (profile.address?.trim()) {
|
|
471
|
+
throw new Error(`token '${tokenKey}' chain '${normalizedChainKey}' must not set address when isNative=true`);
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
normalized.address = assertValidEvmAddress(
|
|
475
|
+
profile.address ?? '',
|
|
476
|
+
`token '${tokenKey}' chain '${normalizedChainKey}' address`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return normalized;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function normalizeTokenProfile(key: string, profile: TokenProfile): TokenProfile {
|
|
484
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
485
|
+
if (!normalizedKey) {
|
|
486
|
+
throw new Error('token profile key is required');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const symbol = profile.symbol?.trim();
|
|
490
|
+
if (!symbol) {
|
|
491
|
+
throw new Error(`token profile '${normalizedKey}' symbol is required`);
|
|
492
|
+
}
|
|
493
|
+
const name = assertOptionalTrimmedString(profile.name, `token profile '${normalizedKey}' name`);
|
|
494
|
+
|
|
495
|
+
const normalizedChains: Record<string, TokenChainProfile> = {};
|
|
496
|
+
for (const [chainKey, chainProfile] of Object.entries(profile.chains ?? {})) {
|
|
497
|
+
const normalizedChainKey = chainKey.trim().toLowerCase();
|
|
498
|
+
if (!normalizedChainKey) {
|
|
499
|
+
throw new Error(`token profile '${normalizedKey}' contains an empty chain key`);
|
|
500
|
+
}
|
|
501
|
+
normalizedChains[normalizedChainKey] = normalizeTokenChainProfile(normalizedKey, normalizedChainKey, chainProfile);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
name,
|
|
506
|
+
symbol,
|
|
507
|
+
defaultPolicy: normalizeTokenPolicyProfile(normalizedKey, 'default', profile.defaultPolicy),
|
|
508
|
+
destinationOverrides: (profile.destinationOverrides ?? []).map((item) =>
|
|
509
|
+
normalizeTokenDestinationOverrideProfile(normalizedKey, item)
|
|
510
|
+
),
|
|
511
|
+
manualApprovalPolicies: (profile.manualApprovalPolicies ?? []).map((item) =>
|
|
512
|
+
normalizeTokenManualApprovalProfile(normalizedKey, item)
|
|
513
|
+
),
|
|
514
|
+
chains: normalizedChains,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function normalizeChainProfileEntry(key: string, profile: ChainProfile): ChainProfile {
|
|
519
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
520
|
+
if (!normalizedKey) {
|
|
521
|
+
throw new Error('chain profile key is required');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const name = profile.name?.trim() || normalizedKey;
|
|
525
|
+
return {
|
|
526
|
+
chainId: assertPositiveSafeInteger(profile.chainId, `chain profile '${normalizedKey}' chainId`),
|
|
527
|
+
name,
|
|
528
|
+
rpcUrl: profile.rpcUrl
|
|
529
|
+
? assertSafeRpcUrl(profile.rpcUrl, `chain profile '${normalizedKey}' rpcUrl`)
|
|
530
|
+
: undefined,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function normalizeChainProfiles(profiles: Record<string, ChainProfile> | undefined): Record<string, ChainProfile> {
|
|
535
|
+
const normalized: Record<string, ChainProfile> = {};
|
|
536
|
+
for (const [key, profile] of Object.entries(profiles ?? {})) {
|
|
537
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
538
|
+
if (!normalizedKey) {
|
|
539
|
+
throw new Error('chain profile key is required');
|
|
540
|
+
}
|
|
541
|
+
normalized[normalizedKey] = normalizeChainProfileEntry(normalizedKey, profile);
|
|
542
|
+
}
|
|
543
|
+
return normalized;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function normalizeTokenProfiles(profiles: Record<string, TokenProfile> | undefined): Record<string, TokenProfile> {
|
|
547
|
+
const normalized: Record<string, TokenProfile> = {};
|
|
548
|
+
for (const [key, profile] of Object.entries(profiles ?? {})) {
|
|
549
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
550
|
+
if (!normalizedKey) {
|
|
551
|
+
throw new Error('token profile key is required');
|
|
552
|
+
}
|
|
553
|
+
normalized[normalizedKey] = normalizeTokenProfile(normalizedKey, profile);
|
|
554
|
+
}
|
|
555
|
+
return normalized;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function normalizePath(targetPath: string): string {
|
|
559
|
+
return path.resolve(targetPath);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function requirePathValue(targetPath: string, label: string): string {
|
|
563
|
+
const normalized = targetPath.trim();
|
|
564
|
+
if (!normalized) {
|
|
565
|
+
throw new Error(`${label} is required`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return normalizePath(normalized);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function readLstat(targetPath: string): fs.Stats | null {
|
|
572
|
+
try {
|
|
573
|
+
return fs.lstatSync(targetPath);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function readLstatAllowInaccessible(targetPath: string): fs.Stats | 'inaccessible' | null {
|
|
583
|
+
try {
|
|
584
|
+
return fs.lstatSync(targetPath);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
587
|
+
if (code === 'ENOENT') {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
591
|
+
return 'inaccessible';
|
|
592
|
+
}
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function isStableRootOwnedSymlink(stats: fs.Stats, targetPath: string): boolean {
|
|
598
|
+
if (process.platform === 'win32' || typeof stats.uid !== 'number' || stats.uid !== 0) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const parentPath = path.dirname(targetPath);
|
|
603
|
+
const parentStats = readLstat(parentPath);
|
|
604
|
+
return Boolean(
|
|
605
|
+
parentStats
|
|
606
|
+
&& parentStats.isDirectory()
|
|
607
|
+
&& typeof parentStats.uid === 'number'
|
|
608
|
+
&& parentStats.uid === 0
|
|
609
|
+
&& (parentStats.mode & GROUP_OTHER_WRITE_MODE_MASK) === 0
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function assertNoSymlinkAncestorDirectories(targetPath: string, label: string): void {
|
|
614
|
+
const normalized = normalizePath(targetPath);
|
|
615
|
+
const parent = path.dirname(normalized);
|
|
616
|
+
if (parent === normalized) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const { root } = path.parse(parent);
|
|
621
|
+
const relativeParent = parent.slice(root.length);
|
|
622
|
+
if (!relativeParent) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let currentPath = root;
|
|
627
|
+
for (const segment of relativeParent.split(path.sep).filter(Boolean)) {
|
|
628
|
+
currentPath = path.join(currentPath, segment);
|
|
629
|
+
const stats = readLstat(currentPath);
|
|
630
|
+
if (!stats) {
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
if (stats.isSymbolicLink()) {
|
|
634
|
+
if (isStableRootOwnedSymlink(stats, currentPath)) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
throw new Error(`${label} '${normalized}' must not traverse symlinked ancestor directories`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function findNearestExistingPath(targetPath: string): { path: string; stats: fs.Stats } {
|
|
643
|
+
let currentPath = normalizePath(targetPath);
|
|
644
|
+
|
|
645
|
+
while (true) {
|
|
646
|
+
const stats = readLstat(currentPath);
|
|
647
|
+
if (stats) {
|
|
648
|
+
return {
|
|
649
|
+
path: currentPath,
|
|
650
|
+
stats
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const parentPath = path.dirname(currentPath);
|
|
655
|
+
if (parentPath === currentPath) {
|
|
656
|
+
throw new Error(`No existing ancestor directory found for '${targetPath}'`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
currentPath = parentPath;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export function allowedOwnerUids(): Set<number> {
|
|
664
|
+
const allowed = new Set<number>();
|
|
665
|
+
const effectiveUid = typeof process.geteuid === 'function' ? process.geteuid() : null;
|
|
666
|
+
if (effectiveUid !== null) {
|
|
667
|
+
allowed.add(effectiveUid);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const sudoUid = process.env.SUDO_UID?.trim();
|
|
671
|
+
if (effectiveUid === 0 && sudoUid && /^\d+$/u.test(sudoUid)) {
|
|
672
|
+
allowed.add(Number(sudoUid));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return allowed;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function assertTrustedOwner(stats: fs.Stats, targetPath: string, label: string): void {
|
|
679
|
+
if (process.platform === 'win32' || typeof stats.uid !== 'number') {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (stats.uid === 0) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const allowed = allowedOwnerUids();
|
|
688
|
+
if (!allowed.has(stats.uid)) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`${label} '${targetPath}' must be owned by the current user, sudo caller, or root`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function assertSecureDirectory(stats: fs.Stats, targetPath: string, label: string): void {
|
|
696
|
+
assertTrustedOwner(stats, targetPath, label);
|
|
697
|
+
if (process.platform === 'win32') {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if ((stats.mode & GROUP_OTHER_WRITE_MODE_MASK) !== 0) {
|
|
702
|
+
throw new Error(`${label} '${targetPath}' must not be writable by group/other`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function isStickyDirectory(stats: fs.Stats): boolean {
|
|
707
|
+
return process.platform !== 'win32' && (stats.mode & STICKY_BIT_MODE) !== 0;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function assertSecureDirectoryPath(targetPath: string, label: string): void {
|
|
711
|
+
const normalized = normalizePath(targetPath);
|
|
712
|
+
assertNoSymlinkAncestorDirectories(normalized, label);
|
|
713
|
+
const targetStats = fs.lstatSync(normalized);
|
|
714
|
+
if (targetStats.isSymbolicLink()) {
|
|
715
|
+
throw new Error(`${label} '${normalized}' must not be a symlink`);
|
|
716
|
+
}
|
|
717
|
+
if (!targetStats.isDirectory()) {
|
|
718
|
+
throw new Error(`${label} '${normalized}' must be a directory`);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
assertSecureDirectory(targetStats, normalized, label);
|
|
722
|
+
|
|
723
|
+
const ancestors: string[] = [];
|
|
724
|
+
let currentPath = fs.realpathSync.native(normalized);
|
|
725
|
+
|
|
726
|
+
while (true) {
|
|
727
|
+
ancestors.push(currentPath);
|
|
728
|
+
const parentPath = path.dirname(currentPath);
|
|
729
|
+
if (parentPath === currentPath) {
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
currentPath = parentPath;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
for (const [index, currentDirectory] of ancestors.entries()) {
|
|
736
|
+
if (index === 0) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const stats = fs.lstatSync(currentDirectory);
|
|
741
|
+
if (!stats.isDirectory()) {
|
|
742
|
+
throw new Error(`${label} '${currentDirectory}' must be a directory`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
assertTrustedOwner(stats, currentDirectory, label);
|
|
746
|
+
if (process.platform !== 'win32' && (stats.mode & GROUP_OTHER_WRITE_MODE_MASK) !== 0) {
|
|
747
|
+
const allowStickyAncestor = index > 0 && isStickyDirectory(stats);
|
|
748
|
+
if (!allowStickyAncestor) {
|
|
749
|
+
throw new Error(`${label} '${currentDirectory}' must not be writable by group/other`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function assertSecureFile(stats: fs.Stats, targetPath: string, label: string): void {
|
|
756
|
+
assertTrustedOwner(stats, targetPath, label);
|
|
757
|
+
if (process.platform === 'win32') {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if ((stats.mode & PRIVATE_FILE_MODE_MASK) !== 0) {
|
|
762
|
+
throw new Error(`${label} '${targetPath}' must not grant group/other permissions`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function assertNotSymlink(targetPath: string, label: string): fs.Stats | null {
|
|
767
|
+
const stats = readLstat(targetPath);
|
|
768
|
+
if (stats?.isSymbolicLink()) {
|
|
769
|
+
throw new Error(`${label} '${targetPath}' must not be a symlink`);
|
|
770
|
+
}
|
|
771
|
+
return stats;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function tightenPermissions(targetPath: string, mode: number) {
|
|
775
|
+
try {
|
|
776
|
+
fs.chmodSync(targetPath, mode);
|
|
777
|
+
} catch {}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function ensurePrivateDirectory(targetPath: string, label: string): string {
|
|
781
|
+
const normalized = normalizePath(targetPath);
|
|
782
|
+
assertNoSymlinkAncestorDirectories(normalized, label);
|
|
783
|
+
const stats = assertNotSymlink(normalized, label);
|
|
784
|
+
if (stats && !stats.isDirectory()) {
|
|
785
|
+
throw new Error(`${label} '${normalized}' must be a directory`);
|
|
786
|
+
}
|
|
787
|
+
if (!stats) {
|
|
788
|
+
fs.mkdirSync(normalized, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
789
|
+
}
|
|
790
|
+
tightenPermissions(normalized, PRIVATE_DIR_MODE);
|
|
791
|
+
assertSecureDirectoryPath(normalized, label);
|
|
792
|
+
return normalized;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function readUtf8FileSecure(targetPath: string, label: string, maxBytes?: number): string {
|
|
796
|
+
const normalized = normalizePath(targetPath);
|
|
797
|
+
assertSecureDirectoryPath(path.dirname(normalized), `${label} parent directory`);
|
|
798
|
+
const openFlags = process.platform === 'win32'
|
|
799
|
+
? fs.constants.O_RDONLY
|
|
800
|
+
: fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
|
|
801
|
+
const fd = fs.openSync(normalized, openFlags);
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
const stats = fs.fstatSync(fd);
|
|
805
|
+
if (!stats.isFile()) {
|
|
806
|
+
throw new Error(`${label} '${normalized}' must be a regular file`);
|
|
807
|
+
}
|
|
808
|
+
assertSecureFile(stats, normalized, label);
|
|
809
|
+
if (maxBytes !== undefined && stats.size > maxBytes) {
|
|
810
|
+
throw new Error(`${label} '${normalized}' must not exceed ${maxBytes} bytes`);
|
|
811
|
+
}
|
|
812
|
+
return fs.readFileSync(fd, 'utf8');
|
|
813
|
+
} finally {
|
|
814
|
+
fs.closeSync(fd);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function readPrivateJsonFile<T>(targetPath: string, label: string): T | null {
|
|
819
|
+
const normalized = normalizePath(targetPath);
|
|
820
|
+
const stats = assertNotSymlink(normalized, label);
|
|
821
|
+
if (!stats) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
if (!stats.isFile()) {
|
|
825
|
+
throw new Error(`${label} '${normalized}' must be a regular file`);
|
|
826
|
+
}
|
|
827
|
+
tightenPermissions(normalized, PRIVATE_FILE_MODE);
|
|
828
|
+
return JSON.parse(readUtf8FileSecure(normalized, label, MAX_CONFIG_FILE_BYTES)) as T;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function assertSecurePlannedDirectoryPath(targetPath: string, label: string): string {
|
|
832
|
+
const normalized = requirePathValue(targetPath, label);
|
|
833
|
+
assertNoSymlinkAncestorDirectories(normalized, label);
|
|
834
|
+
const stats = readLstat(normalized);
|
|
835
|
+
|
|
836
|
+
if (stats) {
|
|
837
|
+
if (stats.isSymbolicLink()) {
|
|
838
|
+
throw new Error(`${label} '${normalized}' must not be a symlink`);
|
|
839
|
+
}
|
|
840
|
+
if (!stats.isDirectory()) {
|
|
841
|
+
throw new Error(`${label} '${normalized}' must be a directory`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
assertSecureDirectoryPath(normalized, label);
|
|
845
|
+
return normalized;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const nearestExistingPath = findNearestExistingPath(normalized);
|
|
849
|
+
if (nearestExistingPath.stats.isSymbolicLink()) {
|
|
850
|
+
throw new Error(`${label} '${nearestExistingPath.path}' must not be a symlink`);
|
|
851
|
+
}
|
|
852
|
+
if (!nearestExistingPath.stats.isDirectory()) {
|
|
853
|
+
throw new Error(`${label} '${nearestExistingPath.path}' must be a directory`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
assertSecureDirectoryPath(nearestExistingPath.path, label);
|
|
857
|
+
return normalized;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function assertSecurePlannedDaemonSocketPath(targetPath: string, label: string): string {
|
|
861
|
+
const normalized = requirePathValue(targetPath, label);
|
|
862
|
+
assertSecurePlannedDirectoryPath(path.dirname(normalized), `${label} directory`);
|
|
863
|
+
|
|
864
|
+
const stats = readLstat(normalized);
|
|
865
|
+
if (!stats) {
|
|
866
|
+
return normalized;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (stats.isSymbolicLink()) {
|
|
870
|
+
throw new Error(`${label} '${normalized}' must not be a symlink`);
|
|
871
|
+
}
|
|
872
|
+
if (process.platform !== 'win32' && !stats.isSocket()) {
|
|
873
|
+
throw new Error(`${label} '${normalized}' must be a unix socket`);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
assertTrustedOwner(stats, normalized, label);
|
|
877
|
+
return normalized;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function assertSecurePlannedPrivateFilePath(targetPath: string, label: string): string {
|
|
881
|
+
const normalized = requirePathValue(targetPath, label);
|
|
882
|
+
assertSecurePlannedDirectoryPath(path.dirname(normalized), `${label} directory`);
|
|
883
|
+
|
|
884
|
+
const stats = readLstatAllowInaccessible(normalized);
|
|
885
|
+
if (!stats || stats === 'inaccessible') {
|
|
886
|
+
return normalized;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (stats.isSymbolicLink()) {
|
|
890
|
+
throw new Error(`${label} '${normalized}' must not be a symlink`);
|
|
891
|
+
}
|
|
892
|
+
if (!stats.isFile()) {
|
|
893
|
+
throw new Error(`${label} '${normalized}' must be a regular file`);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
assertSecureFile(stats, normalized, label);
|
|
897
|
+
return normalized;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function writePrivateFile(targetPath: string, contents: string, label: string) {
|
|
901
|
+
const normalized = normalizePath(targetPath);
|
|
902
|
+
const parent = ensurePrivateDirectory(path.dirname(normalized), `${label} parent`);
|
|
903
|
+
assertNotSymlink(normalized, label);
|
|
904
|
+
|
|
905
|
+
const tempPath = path.join(
|
|
906
|
+
parent,
|
|
907
|
+
`.${path.basename(normalized)}.tmp-${process.pid}-${Date.now()}`
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
try {
|
|
911
|
+
fs.writeFileSync(tempPath, contents, {
|
|
912
|
+
encoding: 'utf8',
|
|
913
|
+
mode: PRIVATE_FILE_MODE,
|
|
914
|
+
flag: 'wx'
|
|
915
|
+
});
|
|
916
|
+
tightenPermissions(tempPath, PRIVATE_FILE_MODE);
|
|
917
|
+
fs.renameSync(tempPath, normalized);
|
|
918
|
+
tightenPermissions(normalized, PRIVATE_FILE_MODE);
|
|
919
|
+
} finally {
|
|
920
|
+
try {
|
|
921
|
+
if (fs.existsSync(tempPath)) {
|
|
922
|
+
fs.rmSync(tempPath);
|
|
923
|
+
}
|
|
924
|
+
} catch {}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function normalizedDefaultConfig(): WlfiConfig {
|
|
929
|
+
return {
|
|
930
|
+
daemonSocket: defaultDaemonSocketPath(),
|
|
931
|
+
stateFile: defaultStateFilePath(),
|
|
932
|
+
rustBinDir: defaultRustBinDir(),
|
|
933
|
+
chains: defaultChainProfiles(),
|
|
934
|
+
tokens: defaultTokenProfiles()
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function applySeedDefaultsIfLegacyEmpty(config: WlfiConfig): WlfiConfig {
|
|
939
|
+
const chainCount = Object.keys(config.chains ?? {}).length;
|
|
940
|
+
const tokenCount = Object.keys(config.tokens ?? {}).length;
|
|
941
|
+
if (chainCount === 0 && tokenCount === 0) {
|
|
942
|
+
return {
|
|
943
|
+
...config,
|
|
944
|
+
chains: defaultChainProfiles(),
|
|
945
|
+
tokens: defaultTokenProfiles(),
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
return config;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export function resolveWlfiHome(): string {
|
|
952
|
+
const explicit = process.env.WLFI_HOME?.trim();
|
|
953
|
+
if (explicit) {
|
|
954
|
+
return normalizePath(explicit);
|
|
955
|
+
}
|
|
956
|
+
return path.join(os.homedir(), WLFI_DIRNAME);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export function resolveConfigPath(): string {
|
|
960
|
+
return path.join(resolveWlfiHome(), CONFIG_FILENAME);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
export function defaultDaemonSocketPath(): string {
|
|
964
|
+
return path.join(resolveWlfiHome(), 'daemon.sock');
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
export function defaultStateFilePath(): string {
|
|
968
|
+
return path.join(resolveWlfiHome(), 'daemon-state.enc');
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export function defaultRustBinDir(): string {
|
|
972
|
+
return path.join(resolveWlfiHome(), 'bin');
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
export function defaultConfig(): WlfiConfig {
|
|
976
|
+
return normalizedDefaultConfig();
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
export function ensureWlfiHome(): string {
|
|
980
|
+
return ensurePrivateDirectory(resolveWlfiHome(), 'WLFI home');
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
export function readConfig(): WlfiConfig {
|
|
984
|
+
ensureWlfiHome();
|
|
985
|
+
const parsed = readPrivateJsonFile<WlfiConfig>(resolveConfigPath(), 'config file');
|
|
986
|
+
const merged = applySeedDefaultsIfLegacyEmpty({
|
|
987
|
+
...normalizedDefaultConfig(),
|
|
988
|
+
...(parsed ?? {})
|
|
989
|
+
} satisfies WlfiConfig);
|
|
990
|
+
|
|
991
|
+
return {
|
|
992
|
+
...merged,
|
|
993
|
+
wallet: normalizeWalletProfile(merged.wallet),
|
|
994
|
+
chains: normalizeChainProfiles(merged.chains),
|
|
995
|
+
tokens: normalizeTokenProfiles(merged.tokens),
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export function writeConfig(nextConfig: WlfiConfig): WlfiConfig {
|
|
1000
|
+
ensureWlfiHome();
|
|
1001
|
+
const merged: WlfiConfig = {
|
|
1002
|
+
...normalizedDefaultConfig(),
|
|
1003
|
+
...readConfig(),
|
|
1004
|
+
...nextConfig
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
if (Object.prototype.hasOwnProperty.call(nextConfig, 'rpcUrl') && merged.rpcUrl !== undefined) {
|
|
1008
|
+
merged.rpcUrl = assertSafeRpcUrl(merged.rpcUrl, 'rpcUrl');
|
|
1009
|
+
}
|
|
1010
|
+
if (Object.prototype.hasOwnProperty.call(nextConfig, 'daemonSocket') && merged.daemonSocket !== undefined) {
|
|
1011
|
+
merged.daemonSocket = assertSecurePlannedDaemonSocketPath(merged.daemonSocket, 'daemonSocket');
|
|
1012
|
+
}
|
|
1013
|
+
if (Object.prototype.hasOwnProperty.call(nextConfig, 'stateFile') && merged.stateFile !== undefined) {
|
|
1014
|
+
merged.stateFile = assertSecurePlannedPrivateFilePath(merged.stateFile, 'stateFile');
|
|
1015
|
+
}
|
|
1016
|
+
if (Object.prototype.hasOwnProperty.call(nextConfig, 'rustBinDir') && merged.rustBinDir !== undefined) {
|
|
1017
|
+
merged.rustBinDir = assertSecurePlannedDirectoryPath(merged.rustBinDir, 'rustBinDir');
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
merged.wallet = normalizeWalletProfile(merged.wallet);
|
|
1021
|
+
merged.chains = normalizeChainProfiles(merged.chains);
|
|
1022
|
+
merged.tokens = normalizeTokenProfiles(merged.tokens);
|
|
1023
|
+
|
|
1024
|
+
writePrivateFile(resolveConfigPath(), JSON.stringify(merged, null, 2) + '\n', 'config file');
|
|
1025
|
+
return merged;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
export function deleteConfigKey(key: keyof WlfiConfig): WlfiConfig {
|
|
1029
|
+
ensureWlfiHome();
|
|
1030
|
+
const current = {
|
|
1031
|
+
...normalizedDefaultConfig(),
|
|
1032
|
+
...readConfig()
|
|
1033
|
+
};
|
|
1034
|
+
delete current[key];
|
|
1035
|
+
|
|
1036
|
+
const normalized = {
|
|
1037
|
+
...normalizedDefaultConfig(),
|
|
1038
|
+
...current,
|
|
1039
|
+
chains: current.chains ?? {},
|
|
1040
|
+
tokens: current.tokens ?? {}
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
writePrivateFile(resolveConfigPath(), JSON.stringify(normalized, null, 2) + '\n', 'config file');
|
|
1044
|
+
return normalized;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export function listBuiltinChains(): ChainProfile[] {
|
|
1048
|
+
return Object.entries(BUILTIN_CHAINS)
|
|
1049
|
+
.map(([key, value]) => ({ ...value, name: key }))
|
|
1050
|
+
.sort((left, right) => left.chainId - right.chainId || left.name.localeCompare(right.name));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export function listBuiltinTokens(): TokenProfileEntry[] {
|
|
1054
|
+
return Object.entries(BUILTIN_TOKENS)
|
|
1055
|
+
.map(([key, value]) => ({
|
|
1056
|
+
key,
|
|
1057
|
+
name: value.name,
|
|
1058
|
+
symbol: value.symbol,
|
|
1059
|
+
chains: Object.entries(value.chains ?? {})
|
|
1060
|
+
.map(([chainKey, chainValue]) => ({ key: chainKey, ...chainValue }))
|
|
1061
|
+
.sort((left, right) => left.chainId - right.chainId || left.key.localeCompare(right.key)),
|
|
1062
|
+
}))
|
|
1063
|
+
.sort((left, right) => left.key.localeCompare(right.key));
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export function listConfiguredTokens(
|
|
1067
|
+
config: WlfiConfig = readConfig()
|
|
1068
|
+
): TokenProfileEntry[] {
|
|
1069
|
+
return Object.entries(config.tokens ?? {})
|
|
1070
|
+
.map(([key, value]) => ({
|
|
1071
|
+
key,
|
|
1072
|
+
name: value.name,
|
|
1073
|
+
symbol: value.symbol,
|
|
1074
|
+
chains: Object.entries(value.chains ?? {})
|
|
1075
|
+
.map(([chainKey, chainValue]) => ({ key: chainKey, ...chainValue }))
|
|
1076
|
+
.sort((left, right) => left.chainId - right.chainId || left.key.localeCompare(right.key)),
|
|
1077
|
+
}))
|
|
1078
|
+
.sort((left, right) => left.key.localeCompare(right.key));
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
export function resolveTokenProfile(
|
|
1082
|
+
selector: string,
|
|
1083
|
+
config: WlfiConfig = readConfig()
|
|
1084
|
+
): (TokenProfile & { key: string; source: 'configured' | 'builtin' }) | null {
|
|
1085
|
+
const normalized = selector.trim().toLowerCase();
|
|
1086
|
+
if (!normalized) {
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
for (const [key, value] of Object.entries(config.tokens ?? {})) {
|
|
1091
|
+
if (key.toLowerCase() === normalized || value.symbol.toLowerCase() === normalized) {
|
|
1092
|
+
return { key, source: 'configured', ...value };
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
for (const [key, value] of Object.entries(BUILTIN_TOKENS)) {
|
|
1097
|
+
if (key.toLowerCase() === normalized || value.symbol.toLowerCase() === normalized) {
|
|
1098
|
+
return { key, source: 'builtin', ...value };
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
export function listConfiguredChains(config: WlfiConfig = readConfig()): Array<ChainProfile & { key: string }> {
|
|
1106
|
+
return Object.entries(config.chains ?? {})
|
|
1107
|
+
.map(([key, value]) => ({ key, ...value }))
|
|
1108
|
+
.sort((left, right) => left.chainId - right.chainId || left.key.localeCompare(right.key));
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export function resolveChainProfile(selector: string, config: WlfiConfig = readConfig()): (ChainProfile & { key?: string; source: 'configured' | 'builtin' | 'active' }) | null {
|
|
1112
|
+
const normalized = selector.trim().toLowerCase();
|
|
1113
|
+
if (!normalized) {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
for (const [key, value] of Object.entries(config.chains ?? {})) {
|
|
1118
|
+
if (key.toLowerCase() === normalized || value.name.toLowerCase() === normalized || String(value.chainId) === selector) {
|
|
1119
|
+
return { ...value, key, source: 'configured' };
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (BUILTIN_CHAINS[normalized]) {
|
|
1124
|
+
return { ...BUILTIN_CHAINS[normalized], key: normalized, source: 'builtin' };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (config.chainId !== undefined && String(config.chainId) === selector) {
|
|
1128
|
+
return {
|
|
1129
|
+
chainId: config.chainId,
|
|
1130
|
+
name: config.chainName ?? normalized,
|
|
1131
|
+
rpcUrl: config.rpcUrl,
|
|
1132
|
+
source: 'active'
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export function saveChainProfile(key: string, profile: ChainProfile): WlfiConfig {
|
|
1140
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
1141
|
+
if (!normalizedKey) {
|
|
1142
|
+
throw new Error('chain profile key is required');
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const normalizedProfile = normalizeChainProfileEntry(normalizedKey, profile);
|
|
1146
|
+
|
|
1147
|
+
return writeConfig({
|
|
1148
|
+
chains: {
|
|
1149
|
+
...(readConfig().chains ?? {}),
|
|
1150
|
+
[normalizedKey]: normalizedProfile
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
export function removeChainProfile(key: string): WlfiConfig {
|
|
1156
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
1157
|
+
const nextChains = { ...(readConfig().chains ?? {}) };
|
|
1158
|
+
delete nextChains[normalizedKey];
|
|
1159
|
+
return writeConfig({ chains: nextChains });
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
export function saveTokenProfile(key: string, profile: TokenProfile): WlfiConfig {
|
|
1163
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
1164
|
+
return writeConfig({
|
|
1165
|
+
tokens: {
|
|
1166
|
+
...(readConfig().tokens ?? {}),
|
|
1167
|
+
[normalizedKey]: normalizeTokenProfile(normalizedKey, profile),
|
|
1168
|
+
},
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
export function saveTokenChainProfile(
|
|
1173
|
+
tokenKey: string,
|
|
1174
|
+
chainKey: string,
|
|
1175
|
+
profile: TokenChainProfile,
|
|
1176
|
+
options: { symbol?: string } = {}
|
|
1177
|
+
): WlfiConfig {
|
|
1178
|
+
const normalizedTokenKey = tokenKey.trim().toLowerCase();
|
|
1179
|
+
const normalizedChainKey = chainKey.trim().toLowerCase();
|
|
1180
|
+
const current = readConfig();
|
|
1181
|
+
const existing = current.tokens?.[normalizedTokenKey];
|
|
1182
|
+
const symbol = options.symbol?.trim() || existing?.symbol;
|
|
1183
|
+
if (!symbol) {
|
|
1184
|
+
throw new Error(`token '${normalizedTokenKey}' symbol is required`);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return writeConfig({
|
|
1188
|
+
tokens: {
|
|
1189
|
+
...(current.tokens ?? {}),
|
|
1190
|
+
[normalizedTokenKey]: normalizeTokenProfile(normalizedTokenKey, {
|
|
1191
|
+
name: existing?.name,
|
|
1192
|
+
symbol,
|
|
1193
|
+
defaultPolicy: existing?.defaultPolicy,
|
|
1194
|
+
destinationOverrides: existing?.destinationOverrides,
|
|
1195
|
+
manualApprovalPolicies: existing?.manualApprovalPolicies,
|
|
1196
|
+
chains: {
|
|
1197
|
+
...(existing?.chains ?? {}),
|
|
1198
|
+
[normalizedChainKey]: normalizeTokenChainProfile(normalizedTokenKey, normalizedChainKey, profile),
|
|
1199
|
+
},
|
|
1200
|
+
}),
|
|
1201
|
+
},
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
export function removeTokenProfile(key: string): WlfiConfig {
|
|
1206
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
1207
|
+
const nextTokens = { ...(readConfig().tokens ?? {}) };
|
|
1208
|
+
delete nextTokens[normalizedKey];
|
|
1209
|
+
return writeConfig({ tokens: nextTokens });
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
export function removeTokenChainProfile(tokenKey: string, chainKey: string): WlfiConfig {
|
|
1213
|
+
const normalizedTokenKey = tokenKey.trim().toLowerCase();
|
|
1214
|
+
const normalizedChainKey = chainKey.trim().toLowerCase();
|
|
1215
|
+
const current = readConfig();
|
|
1216
|
+
const token = current.tokens?.[normalizedTokenKey];
|
|
1217
|
+
if (!token) {
|
|
1218
|
+
return current;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const nextChains = { ...(token.chains ?? {}) };
|
|
1222
|
+
delete nextChains[normalizedChainKey];
|
|
1223
|
+
const nextTokens = { ...(current.tokens ?? {}) };
|
|
1224
|
+
if (Object.keys(nextChains).length === 0) {
|
|
1225
|
+
delete nextTokens[normalizedTokenKey];
|
|
1226
|
+
} else {
|
|
1227
|
+
nextTokens[normalizedTokenKey] = normalizeTokenProfile(normalizedTokenKey, {
|
|
1228
|
+
name: token.name,
|
|
1229
|
+
symbol: token.symbol,
|
|
1230
|
+
defaultPolicy: token.defaultPolicy,
|
|
1231
|
+
destinationOverrides: token.destinationOverrides,
|
|
1232
|
+
manualApprovalPolicies: token.manualApprovalPolicies,
|
|
1233
|
+
chains: nextChains,
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
return writeConfig({ tokens: nextTokens });
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
export function switchActiveChain(selector: string, options: { rpcUrl?: string; persistProfile?: boolean } = {}): WlfiConfig {
|
|
1240
|
+
const config = readConfig();
|
|
1241
|
+
const profile = resolveChainProfile(selector, config);
|
|
1242
|
+
if (!profile) {
|
|
1243
|
+
throw new Error(`Unknown chain selector: ${selector}`);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const nextRpcUrl = options.rpcUrl ?? profile.rpcUrl;
|
|
1247
|
+
const normalizedRpcUrl = nextRpcUrl ? assertSafeRpcUrl(nextRpcUrl, 'rpcUrl') : undefined;
|
|
1248
|
+
let next = writeConfig({
|
|
1249
|
+
chainId: profile.chainId,
|
|
1250
|
+
chainName: profile.name,
|
|
1251
|
+
rpcUrl: normalizedRpcUrl,
|
|
1252
|
+
chains: config.chains ?? {}
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
if (options.persistProfile) {
|
|
1256
|
+
next = saveChainProfile(profile.key ?? profile.name, {
|
|
1257
|
+
chainId: profile.chainId,
|
|
1258
|
+
name: profile.name,
|
|
1259
|
+
rpcUrl: normalizedRpcUrl
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return next;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
export function redactConfig(config: WlfiConfig): Record<string, unknown> {
|
|
1267
|
+
return {
|
|
1268
|
+
...config,
|
|
1269
|
+
agentAuthToken: config.agentAuthToken ? '<redacted>' : undefined,
|
|
1270
|
+
paths: {
|
|
1271
|
+
wlfiHome: resolveWlfiHome(),
|
|
1272
|
+
configPath: resolveConfigPath(),
|
|
1273
|
+
daemonSocket: config.daemonSocket ?? defaultDaemonSocketPath(),
|
|
1274
|
+
stateFile: config.stateFile ?? defaultStateFilePath(),
|
|
1275
|
+
rustBinDir: config.rustBinDir ?? defaultRustBinDir()
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
export function resolveRustBinaryPath(binaryName: string, config: WlfiConfig = readConfig()): string {
|
|
1281
|
+
return path.join(config.rustBinDir ?? defaultRustBinDir(), binaryName + (process.platform === 'win32' ? '.exe' : ''));
|
|
1282
|
+
}
|