@thirdfy/agent-cli 0.4.2 → 0.4.3
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/CHANGELOG.md +19 -0
- package/README.md +4 -6
- package/package.json +3 -1
- package/src/cli/help.mjs +1 -1
- package/src/cli/options/onboarding/credentials.mjs +7 -5
- package/src/commands/doctor.mjs +5 -5
- package/src/commands/execute.mjs +2 -11
- package/src/commands/onboarding/credentials.mjs +12 -25
- package/src/core/chainRegistry.mjs +11 -0
- package/src/core/constants.mjs +0 -19
- package/src/runtime/actions/selection.mjs +0 -1
- package/src/runtime/execution/runners/createExecutionRunners.mjs +0 -3
- package/src/runtime/execution/runners/selfLane.mjs +6 -5
- package/src/runtime/handlers.mjs +0 -4
- package/src/runtime/main.mjs +0 -10
- package/src/runtime/owsSigning.mjs +17 -16
- package/src/runtime/providerHints/cex.mjs +1 -52
- package/src/runtime/bitfinexProbe.mjs +0 -121
- package/src/runtime/execution/runners/fallbackHints.mjs +0 -54
- package/src/runtime/onboardingHints.mjs +0 -54
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.4.3] - 2026-10-14
|
|
8
|
+
|
|
9
|
+
Bitfinex support is removed, ethers loads again for local signing and RPC checks, and an unreachable RPC no longer floods `--json` output. Older `credentials upsert` scripts keep working: the removed flags are accepted and ignored.
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- **Bitfinex support.** It was unused and extra code to maintain. Removed:
|
|
14
|
+
- the Bitfinex static provider hint (`actions --provider bitfinex` no longer carries one; thirdfy-mcp removed its hint too, thirdfy/thirdfy-mcp#270);
|
|
15
|
+
- the `credentials upsert` permission probe, and the `bitfinex:funding-primary` / `bitfinex:spot-primary` credential-ref defaults (pass `--credential-ref` yourself);
|
|
16
|
+
- the Bitfinex onboarding hint (`data.onboardingHint` on error envelopes and on `preflight` / `run` results);
|
|
17
|
+
- `src/runtime/bitfinexProbe.mjs`, `src/runtime/onboardingHints.mjs`, `src/runtime/execution/runners/fallbackHints.mjs`, and the Bitfinex docs.
|
|
18
|
+
|
|
19
|
+
`credentials upsert` and `credentials status` stay for the other venues (Hummingbot connectors, Polymarket, Hyperliquid, Lighter, Avantis). The probe's flags (`--permission-profile`, `--verify-permissions`, `--skip-permissions-probe`, `--bitfinex-base-url`) are still accepted, ignored, and named in `meta.warnings`, so older scripts keep working; they go in the next minor version. The API still serves the Bitfinex catalog rows until it removes them.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **`loadEthers` loads ethers again.** `src/runtime/owsSigning.mjs` called `require` in an ES module, where it is undefined, and the `ReferenceError` was swallowed. So ethers never loaded, and these failed: `doctor self`'s `ethers-runtime` and `rpc-connectivity` checks, OWS sign-and-broadcast, the self lane's effect-check balance reads, and `wallet submit`. It now loads through `createRequire(import.meta.url)`.
|
|
24
|
+
- **RPC calls fail once instead of retrying forever.** Every RPC connection (`doctor self`, OWS broadcast, `wallet submit`, effect-check balance reads) is now pinned to its chain id (`pinnedRpcProvider` in `src/core/chainRegistry.mjs`). An unreachable RPC is one error, not an ethers retry line on stdout every second that breaks `--json` output and keeps the process running. `doctor self`'s `rpc-chain-id` check asks the RPC itself (`eth_chainId`), so a wrong RPC is still reported. Tests: `test/runtime/owsSigning.test.cjs`.
|
|
25
|
+
|
|
7
26
|
## [0.4.2] - 2026-10-14
|
|
8
27
|
|
|
9
28
|
A cleanup release: two small `wallet profile` / `pricing quote` fixes from the 0.4.1 review, and source files moved into domain folders. Commands answer as in 0.4.1, apart from the two fixes.
|
package/README.md
CHANGED
|
@@ -22,7 +22,6 @@ If you want the full developer docs, start here:
|
|
|
22
22
|
- [Action inventory and compatibility](./docs/action-inventory-and-compatibility.md)
|
|
23
23
|
- [Command reference](./docs/command-reference.md)
|
|
24
24
|
- [DogeOS quickstart (repo)](./docs/dogeos-quickstart.md)
|
|
25
|
-
- [Bitfinex CEX workflow (repo)](./docs/bitfinex-cex-workflow.md)
|
|
26
25
|
- [Action onboarding standard](./docs/action-onboarding-standard.md)
|
|
27
26
|
- [Provider docs governance model](./docs/provider-docs-governance.md)
|
|
28
27
|
- [Skill integration RFC](./docs/skill-integration-rfc.md)
|
|
@@ -40,11 +39,11 @@ Run without global install:
|
|
|
40
39
|
npx @thirdfy/agent-cli --help
|
|
41
40
|
```
|
|
42
41
|
|
|
43
|
-
## What's new in v0.4.
|
|
42
|
+
## What's new in v0.4.3
|
|
44
43
|
|
|
45
|
-
- **
|
|
46
|
-
-
|
|
47
|
-
- **
|
|
44
|
+
- **Local signing works again:** ethers now loads, so `doctor self`, OWS sign-and-broadcast, `wallet submit` and the self lane's balance checks run as documented.
|
|
45
|
+
- **A dead RPC is one clear error:** every RPC call is pinned to its chain, so an unreachable RPC no longer prints retry lines into `--json` output or keeps the command running.
|
|
46
|
+
- **Bitfinex support removed from the CLI:** no Bitfinex provider hint, credential permission probe or docs any more. The Thirdfy API still serves the Bitfinex actions for now. `credentials upsert` still works for the other venues, and its old Bitfinex flags are accepted and ignored with a warning.
|
|
48
47
|
- Older versions: see [CHANGELOG.md](./CHANGELOG.md).
|
|
49
48
|
|
|
50
49
|
## Quick start
|
|
@@ -346,7 +345,6 @@ thirdfy-agent run --agent-api-key "$THIRDFY_AGENT_API_KEY" --action swap \
|
|
|
346
345
|
Provider and venue guides are kept outside `README` so this page stays stable as integrations grow.
|
|
347
346
|
|
|
348
347
|
- Provider index: [`docs/providers/index.md`](./docs/providers/index.md)
|
|
349
|
-
- Bitfinex runbook: [`docs/providers/bitfinex.md`](./docs/providers/bitfinex.md)
|
|
350
348
|
- DogeOS runbook: [`docs/providers/dogeos.md`](./docs/providers/dogeos.md)
|
|
351
349
|
|
|
352
350
|
## Command groups
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thirdfy/agent-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"e2e:local-api:smoke": "node ./scripts/e2e/local-api-smoke.mjs",
|
|
37
37
|
"e2e:base:simulate": "node ./scripts/e2e/base-simulate.mjs",
|
|
38
38
|
"e2e:base:real": "node ./scripts/e2e/base-realtx.mjs",
|
|
39
|
+
"e2e:agent-wallet:simulate": "node ./scripts/e2e/agent-wallet/run.mjs",
|
|
40
|
+
"e2e:agent-wallet:real": "node ./scripts/e2e/agent-wallet/run.mjs --real",
|
|
39
41
|
"e2e:delegation:discovery": "node ./scripts/e2e/delegation-discovery.mjs",
|
|
40
42
|
"e2e:delegation:lifecycle": "node ./scripts/e2e/delegation-lifecycle.mjs",
|
|
41
43
|
"e2e:delegation:commands": "node ./scripts/e2e/delegation-command-parity.mjs",
|
package/src/cli/help.mjs
CHANGED
|
@@ -54,7 +54,7 @@ Core commands:
|
|
|
54
54
|
thirdfy-agent wallet sign --agent-api-key <key> --action <id> --params <json> [--chain-id <id>] [--json]
|
|
55
55
|
thirdfy-agent wallet submit --signed-tx-hex <hex> [--chain-id <id>] [--rpc-url <url>] [--json]
|
|
56
56
|
thirdfy-agent agent run --agent-api-key <key> --action <id> --params <json> [--signer-method managed_wallet_server|byow|fanout_intent] [--strategy-intent single_wallet|fanout] [--run-mode <mode>] [--user-did <did>] [--json]
|
|
57
|
-
thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--
|
|
57
|
+
thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--credential-ref <ref>] [--json]
|
|
58
58
|
thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
|
|
59
59
|
thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
|
|
60
60
|
thirdfy-agent agent auth verify --challenge-id <id> --signature <hex> --agent-key <key> [--wallet-address <addr>] [--json]
|
|
@@ -3,13 +3,15 @@ export function attachCredentialsOptions(cmd) {
|
|
|
3
3
|
.option('--venue <id>', 'credential venue')
|
|
4
4
|
.option('--api-key <key>', 'exchange API key')
|
|
5
5
|
.option('--api-secret <secret>', 'exchange API secret')
|
|
6
|
-
.option('--permission-profile <profile>', 'funding|spot')
|
|
7
6
|
.option('--credential-ref <ref>', 'credential reference')
|
|
8
7
|
.option('--credential-type <type>', 'credential type')
|
|
9
|
-
.option('--verify-permissions', 'verify exchange permissions')
|
|
10
|
-
.option('--skip-permissions-probe', 'skip permissions probe')
|
|
11
|
-
.option('--bitfinex-base-url <url>', 'Bitfinex API base URL')
|
|
12
8
|
.option('--passphrase <value>', 'exchange passphrase')
|
|
13
9
|
.option('--metadata <json>', 'credential metadata JSON')
|
|
14
|
-
.option('--policy <json>', 'credential policy JSON')
|
|
10
|
+
.option('--policy <json>', 'credential policy JSON')
|
|
11
|
+
// Accepted and ignored since Bitfinex support was removed, so older scripts keep working; the
|
|
12
|
+
// command says so in meta.warnings. Drop them in the next minor version.
|
|
13
|
+
.option('--permission-profile <profile>', 'ignored (Bitfinex support was removed)')
|
|
14
|
+
.option('--verify-permissions', 'ignored (Bitfinex support was removed)')
|
|
15
|
+
.option('--skip-permissions-probe', 'ignored (Bitfinex support was removed)')
|
|
16
|
+
.option('--bitfinex-base-url <url>', 'ignored (Bitfinex support was removed)');
|
|
15
17
|
}
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { requireFlag } from '../core/args.mjs';
|
|
|
3
3
|
import { apiGet, requestWithRouteFallback } from '../core/http.mjs';
|
|
4
4
|
import { loadProfileConfig, getProfileConfigPath, getCachedActionsCatalog } from '../core/context.mjs';
|
|
5
5
|
import { DEFAULT_API_BASE, PROFILE_DEFAULTS } from '../core/constants.mjs';
|
|
6
|
-
import { describeChainGas, parseChainIdFlag, resolveChainRpcUrl } from '../core/chainRegistry.mjs';
|
|
6
|
+
import { DEFAULT_CHAIN_ID, describeChainGas, parseChainIdFlag, pinnedRpcProvider, resolveChainRpcUrl } from '../core/chainRegistry.mjs';
|
|
7
7
|
import { exitAfterOutput } from '../core/envelope.mjs';
|
|
8
8
|
import { normalizeRunMode, normalizeManagedRunMode } from '../core/runMode.mjs';
|
|
9
9
|
import { buildExecutionCertificationReport } from '../runtime/certification/executionCertify.mjs';
|
|
@@ -63,13 +63,13 @@ async function commandDoctorSelf(ctx, flags) {
|
|
|
63
63
|
try {
|
|
64
64
|
if (!rpcUrl) throw new Error(`no known RPC for chain ${flags.chainId}; pass --rpc-url <url>`);
|
|
65
65
|
const ethers = await loadEthers();
|
|
66
|
-
const
|
|
66
|
+
const expectedChainId = parseChainIdFlag(flags.chainId);
|
|
67
|
+
const provider = pinnedRpcProvider(ethers, { rpcUrl, chainId: expectedChainId || DEFAULT_CHAIN_ID });
|
|
67
68
|
const block = await provider.getBlockNumber();
|
|
68
69
|
checks.push({ name: 'rpc-connectivity', ok: Number.isFinite(block), detail: `latestBlock=${block}` });
|
|
69
|
-
const expectedChainId = parseChainIdFlag(flags.chainId);
|
|
70
70
|
if (expectedChainId) {
|
|
71
|
-
|
|
72
|
-
const rpcChainId = Number(
|
|
71
|
+
// The provider's network is pinned, so ask the RPC itself which chain it serves.
|
|
72
|
+
const rpcChainId = Number(await provider.send('eth_chainId', []));
|
|
73
73
|
checks.push({
|
|
74
74
|
name: 'rpc-chain-id',
|
|
75
75
|
ok: rpcChainId === expectedChainId,
|
package/src/commands/execute.mjs
CHANGED
|
@@ -33,7 +33,6 @@ export function createExecuteCommands(deps) {
|
|
|
33
33
|
runPreflightByMode,
|
|
34
34
|
runExecutionByMode,
|
|
35
35
|
resolveManagedExecutionPreflight,
|
|
36
|
-
applyExecutionFallbackHints,
|
|
37
36
|
performSelfBroadcast,
|
|
38
37
|
executeManagedWalletRun,
|
|
39
38
|
executeSelfRun,
|
|
@@ -57,11 +56,7 @@ async function commandPreflight(ctx, flags, capabilities) {
|
|
|
57
56
|
|| (await applyLighterAgentWalletAddressDefault(ctx, flags, resolved, runMode))
|
|
58
57
|
|| (await applyAvantisAgentWalletAddressDefault(ctx, flags, resolved, runMode));
|
|
59
58
|
const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved, { hybridWalletMode, managedPreflight });
|
|
60
|
-
const normalized =
|
|
61
|
-
flags,
|
|
62
|
-
runMode,
|
|
63
|
-
resolvedAction: resolved.resolvedAction,
|
|
64
|
-
});
|
|
59
|
+
const normalized = backendResult.normalized;
|
|
65
60
|
const data = flags.__polymarketSetupGuide
|
|
66
61
|
? {
|
|
67
62
|
...normalized,
|
|
@@ -195,11 +190,7 @@ async function commandRun(ctx, flags, capabilities) {
|
|
|
195
190
|
hybridWalletMode,
|
|
196
191
|
managedPreflight,
|
|
197
192
|
});
|
|
198
|
-
const normalized =
|
|
199
|
-
flags,
|
|
200
|
-
runMode,
|
|
201
|
-
resolvedAction: resolved.resolvedAction,
|
|
202
|
-
});
|
|
193
|
+
const normalized = backendResult.normalized;
|
|
203
194
|
if (runMode === 'self' && parseBooleanFlag(flags.broadcast, false) && normalized?.success) {
|
|
204
195
|
const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, normalized);
|
|
205
196
|
printEnvelope({
|
|
@@ -2,47 +2,34 @@ import { createRequire } from 'module';
|
|
|
2
2
|
import { requireFlag, parseJsonFlag } from '../../core/args.mjs';
|
|
3
3
|
import { requestWithRouteFallback } from '../../core/http.mjs';
|
|
4
4
|
import { createCliError } from '../../core/envelope.mjs';
|
|
5
|
-
import { BITFINEX_PERMISSION_PROFILES, DEFAULT_BITFINEX_BASE_URL } from '../../core/constants.mjs';
|
|
6
|
-
import { parseBooleanFlag } from '../../core/runMode.mjs';
|
|
7
5
|
|
|
8
6
|
const require = createRequire(import.meta.url);
|
|
9
7
|
const { normalizeCredentialStatus } = require('../../utils/cli/delegationNormalization.cjs');
|
|
10
|
-
|
|
8
|
+
/** `credentials upsert` flags of the removed Bitfinex permission probe: accepted, ignored, and named in a warning. */
|
|
9
|
+
const REMOVED_BITFINEX_FLAGS = [
|
|
10
|
+
['permissionProfile', '--permission-profile'],
|
|
11
|
+
['verifyPermissions', '--verify-permissions'],
|
|
12
|
+
['skipPermissionsProbe', '--skip-permissions-probe'],
|
|
13
|
+
['bitfinexBaseUrl', '--bitfinex-base-url'],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function createCredentialsCommands({ printEnvelope, getAuthToken, buildOwnerAuthHeaders }) {
|
|
11
17
|
async function commandCredentialsUpsert(ctx, flags, capabilities) {
|
|
12
18
|
const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credentials upsert');
|
|
13
19
|
const venue = requireFlag(flags, 'venue', 'Missing --venue');
|
|
14
|
-
const permissionProfile = String(flags.permissionProfile || 'funding').trim().toLowerCase();
|
|
15
|
-
const bitfinexProfile =
|
|
16
|
-
String(venue).trim().toLowerCase() === 'bitfinex'
|
|
17
|
-
? BITFINEX_PERMISSION_PROFILES[permissionProfile] || BITFINEX_PERMISSION_PROFILES.funding
|
|
18
|
-
: null;
|
|
19
20
|
const payload = {
|
|
20
21
|
venue,
|
|
21
22
|
apiKey: requireFlag(flags, 'apiKey', 'Missing --api-key'),
|
|
22
23
|
apiSecret: requireFlag(flags, 'apiSecret', 'Missing --api-secret'),
|
|
23
24
|
credentialType: String(flags.credentialType || 'api_key'),
|
|
24
|
-
credentialRef: String(flags.credentialRef ||
|
|
25
|
+
credentialRef: String(flags.credentialRef || ''),
|
|
25
26
|
};
|
|
26
27
|
if (flags.passphrase) payload.passphrase = String(flags.passphrase);
|
|
27
28
|
if (flags.metadata) payload.metadata = parseJsonFlag(flags, 'metadata');
|
|
28
29
|
if (flags.policy) payload.policy = parseJsonFlag(flags, 'policy');
|
|
29
30
|
if (flags.agentKey) payload.agentKey = String(flags.agentKey).trim();
|
|
30
31
|
if (flags.chainId) payload.chainId = Number(flags.chainId);
|
|
31
|
-
const
|
|
32
|
-
String(venue).trim().toLowerCase() === 'bitfinex' &&
|
|
33
|
-
!parseBooleanFlag(flags.skipPermissionsProbe, false) &&
|
|
34
|
-
parseBooleanFlag(flags.verifyPermissions, true);
|
|
35
|
-
let permissionProbe = null;
|
|
36
|
-
if (probeEnabled) {
|
|
37
|
-
permissionProbe = await probeBitfinexPermissions({
|
|
38
|
-
apiKey: payload.apiKey,
|
|
39
|
-
apiSecret: payload.apiSecret,
|
|
40
|
-
permissionProfile: bitfinexProfile?.id || 'funding',
|
|
41
|
-
timeoutMs: ctx.timeoutMs,
|
|
42
|
-
baseUrl: String(flags.bitfinexBaseUrl || process.env.BITFINEX_BASE_URL || DEFAULT_BITFINEX_BASE_URL),
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
32
|
+
const ignoredFlags = REMOVED_BITFINEX_FLAGS.filter(([key]) => flags[key] !== undefined).map(([, flag]) => flag);
|
|
46
33
|
const response = await requestWithRouteFallback(ctx, {
|
|
47
34
|
method: 'POST',
|
|
48
35
|
routes: ['/api/v1/agent/delegation/credentials/upsert', '/api/v1/agent/agent/delegation/credentials/upsert'],
|
|
@@ -58,8 +45,8 @@ async function commandCredentialsUpsert(ctx, flags, capabilities) {
|
|
|
58
45
|
data: response,
|
|
59
46
|
meta: {
|
|
60
47
|
apiBase: ctx.apiBase,
|
|
61
|
-
permissionProbe,
|
|
62
48
|
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
49
|
+
...(ignoredFlags.length ? { warnings: [`Ignored ${ignoredFlags.join(', ')}: Bitfinex support was removed. Drop these flags; they will stop being accepted in the next minor version.`] } : {}),
|
|
63
50
|
},
|
|
64
51
|
});
|
|
65
52
|
}
|
|
@@ -293,6 +293,17 @@ export function resolveChainRpcUrl({ chainId, rpcUrl, env = process.env } = {})
|
|
|
293
293
|
return getChainInfo(id)?.rpcUrl || '';
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* An ethers JsonRpcProvider pinned to `chainId` (`staticNetwork`). Without a pinned network, ethers
|
|
298
|
+
* probes the RPC for its chain and, when the RPC is down, prints a retry line to stdout every second
|
|
299
|
+
* and keeps the process alive, which breaks `--json` output. Pinned, a dead RPC is one rejected
|
|
300
|
+
* call. `getNetwork()` then echoes the pin; read the RPC's real chain with `send('eth_chainId', [])`.
|
|
301
|
+
*/
|
|
302
|
+
export function pinnedRpcProvider(ethers, { rpcUrl, chainId }) {
|
|
303
|
+
const id = positiveChainId(chainId) || DEFAULT_CHAIN_ID;
|
|
304
|
+
return new ethers.JsonRpcProvider(rpcUrl, id, { staticNetwork: true });
|
|
305
|
+
}
|
|
306
|
+
|
|
296
307
|
/**
|
|
297
308
|
* Parse a --chain-id value. Returns null when absent, a positive integer when valid,
|
|
298
309
|
* and NaN when present but not a positive integer (the caller rejects it).
|
package/src/core/constants.mjs
CHANGED
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
export const DEFAULT_API_BASE = 'https://api.thirdfy.com';
|
|
2
2
|
export const DEFAULT_TIMEOUT_MS = 30000;
|
|
3
3
|
export const DEFAULT_CATALOG_CACHE_TTL_MS = 60_000;
|
|
4
|
-
export const DEFAULT_BITFINEX_BASE_URL = 'https://api.bitfinex.com';
|
|
5
|
-
export const BITFINEX_PERMISSION_PROFILES = {
|
|
6
|
-
funding: {
|
|
7
|
-
id: 'funding',
|
|
8
|
-
credentialRef: 'bitfinex:funding-primary',
|
|
9
|
-
required: {
|
|
10
|
-
funding: { read: 1, write: 1 },
|
|
11
|
-
wallets: { read: 1, write: 0 },
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
spot: {
|
|
15
|
-
id: 'spot',
|
|
16
|
-
credentialRef: 'bitfinex:spot-primary',
|
|
17
|
-
required: {
|
|
18
|
-
orders: { read: 1, write: 1 },
|
|
19
|
-
wallets: { read: 1, write: 0 },
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
4
|
export const RUN_MODES = ['thirdfy', 'self', 'hybrid', 'agent_wallet'];
|
|
24
5
|
export const HYBRID_WALLET_MODES = ['self', 'agent_wallet'];
|
|
25
6
|
export const CUSTODY_MODES = ['managed', 'external'];
|
|
@@ -222,7 +222,6 @@ function inferActionProvider(action) {
|
|
|
222
222
|
if (key.includes('lighter')) return 'lighter';
|
|
223
223
|
if (key.includes('avantis') && key.includes('vault')) return 'avantis-vault';
|
|
224
224
|
if (key.includes('avantis')) return 'avantis';
|
|
225
|
-
if (key.includes('bitfinex')) return 'bitfinex';
|
|
226
225
|
if (key.includes('polymarket') || key.includes('prediction')) return 'polymarket';
|
|
227
226
|
if (key.includes('vaults-fyi')) return 'vaults-fyi';
|
|
228
227
|
if (key.includes('yield-xyz')) return 'yield-xyz';
|
|
@@ -3,7 +3,6 @@ import { normalizeHybridWalletMode } from '../../../core/runMode.mjs';
|
|
|
3
3
|
import { apiPost } from '../../../core/http.mjs';
|
|
4
4
|
import { actionNameForApi } from '../../actions/selection.mjs';
|
|
5
5
|
import { isReadOnlyAction } from './managedResponse.mjs';
|
|
6
|
-
import { createFallbackHints } from './fallbackHints.mjs';
|
|
7
6
|
import { createManagedPreflight } from './managedPreflight.mjs';
|
|
8
7
|
import { createThirdfyLane } from './thirdfyLane.mjs';
|
|
9
8
|
import { createSelfLane } from './selfLane.mjs';
|
|
@@ -20,7 +19,6 @@ export function createExecutionRunners(deps) {
|
|
|
20
19
|
buildIntentPayload,
|
|
21
20
|
shouldUseBuildTxPreflight,
|
|
22
21
|
} = deps;
|
|
23
|
-
const { applyExecutionFallbackHints } = createFallbackHints(deps);
|
|
24
22
|
const { shouldUseManagedReadFastPath, resolveManagedExecutionPreflight } = createManagedPreflight(deps);
|
|
25
23
|
const { executeThirdfyRun, runReadOnlyActionViaIntentRail } = createThirdfyLane(deps);
|
|
26
24
|
const { executeSelfRun, performSelfBroadcast, resolveEffectCheckResult, submitSignedTx } = createSelfLane(deps, { runReadOnlyActionViaIntentRail });
|
|
@@ -300,7 +298,6 @@ async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
|
|
|
300
298
|
}
|
|
301
299
|
|
|
302
300
|
return {
|
|
303
|
-
applyExecutionFallbackHints,
|
|
304
301
|
runPreflightByMode,
|
|
305
302
|
runExecutionByMode,
|
|
306
303
|
executeThirdfyRun,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createCliError } from '../../../core/envelope.mjs';
|
|
2
2
|
import { normalizeEffectCheckMode } from '../../../core/runMode.mjs';
|
|
3
3
|
import { apiPost } from '../../../core/http.mjs';
|
|
4
|
-
import { resolveChainRpcUrl } from '../../../core/chainRegistry.mjs';
|
|
4
|
+
import { resolveChainRpcUrl, pinnedRpcProvider } from '../../../core/chainRegistry.mjs';
|
|
5
5
|
import { actionNameForApi } from '../../actions/selection.mjs';
|
|
6
6
|
import { isReadOnlyAction } from './managedResponse.mjs';
|
|
7
7
|
import { createRequire } from 'module';
|
|
@@ -103,7 +103,7 @@ async function performSelfBroadcast(ctx, flags, resolvedAction, selfResult) {
|
|
|
103
103
|
|
|
104
104
|
let balancesBefore = null;
|
|
105
105
|
if (effectMode !== 'off' && effectTokenIn && effectTokenOut) {
|
|
106
|
-
balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
|
|
106
|
+
balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut, chainId);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
let submission;
|
|
@@ -136,6 +136,7 @@ async function performSelfBroadcast(ctx, flags, resolvedAction, selfResult) {
|
|
|
136
136
|
const effectCheck = await resolveEffectCheckResult({
|
|
137
137
|
mode: effectMode,
|
|
138
138
|
rpcUrl,
|
|
139
|
+
chainId,
|
|
139
140
|
walletAddress,
|
|
140
141
|
tokenIn: effectTokenIn,
|
|
141
142
|
tokenOut: effectTokenOut,
|
|
@@ -168,6 +169,7 @@ async function performSelfBroadcast(ctx, flags, resolvedAction, selfResult) {
|
|
|
168
169
|
async function resolveEffectCheckResult({
|
|
169
170
|
mode,
|
|
170
171
|
rpcUrl,
|
|
172
|
+
chainId,
|
|
171
173
|
walletAddress,
|
|
172
174
|
tokenIn,
|
|
173
175
|
tokenOut,
|
|
@@ -193,7 +195,7 @@ async function resolveEffectCheckResult({
|
|
|
193
195
|
const maxAttempts = Math.max(1, Number(retries || 0) + 1);
|
|
194
196
|
let last = null;
|
|
195
197
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
196
|
-
const balancesAfter = await readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut);
|
|
198
|
+
const balancesAfter = await readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut, chainId);
|
|
197
199
|
const inDeltaRaw = (BigInt(balancesBefore.tokenIn) - BigInt(balancesAfter.tokenIn)).toString();
|
|
198
200
|
const outDeltaRaw = (BigInt(balancesAfter.tokenOut) - BigInt(balancesBefore.tokenOut)).toString();
|
|
199
201
|
const hasEffect = BigInt(inDeltaRaw) > 0n || BigInt(outDeltaRaw) > 0n;
|
|
@@ -217,8 +219,7 @@ async function resolveEffectCheckResult({
|
|
|
217
219
|
|
|
218
220
|
async function submitSignedTx({ rpcUrl, chainId, signedTxHex }) {
|
|
219
221
|
const ethers = await loadEthers();
|
|
220
|
-
const {
|
|
221
|
-
const provider = new JsonRpcProvider(rpcUrl, chainId);
|
|
222
|
+
const provider = pinnedRpcProvider(ethers, { rpcUrl, chainId });
|
|
222
223
|
const sent = await provider.broadcastTransaction(String(signedTxHex));
|
|
223
224
|
return sent?.hash || null;
|
|
224
225
|
}
|
package/src/runtime/handlers.mjs
CHANGED
|
@@ -35,7 +35,6 @@ export function createRuntimeHandlers(deps) {
|
|
|
35
35
|
maskSecret,
|
|
36
36
|
getAuthToken,
|
|
37
37
|
buildOwnerAuthHeaders,
|
|
38
|
-
probeBitfinexPermissions,
|
|
39
38
|
applyActionFilters,
|
|
40
39
|
getProviderHintForActions,
|
|
41
40
|
resolveChainSupport,
|
|
@@ -51,7 +50,6 @@ export function createRuntimeHandlers(deps) {
|
|
|
51
50
|
submitSignedTx,
|
|
52
51
|
runPreflightByMode,
|
|
53
52
|
runExecutionByMode,
|
|
54
|
-
applyExecutionFallbackHints,
|
|
55
53
|
performSelfBroadcast,
|
|
56
54
|
resolveManagedExecutionPreflight,
|
|
57
55
|
resolveEffectiveUserDid,
|
|
@@ -97,7 +95,6 @@ export function createRuntimeHandlers(deps) {
|
|
|
97
95
|
printEnvelope,
|
|
98
96
|
getAuthToken,
|
|
99
97
|
buildOwnerAuthHeaders,
|
|
100
|
-
probeBitfinexPermissions,
|
|
101
98
|
});
|
|
102
99
|
|
|
103
100
|
const __walletControl = createWalletControlCommands({ printEnvelope, buildOwnerAuthHeaders });
|
|
@@ -173,7 +170,6 @@ export function createRuntimeHandlers(deps) {
|
|
|
173
170
|
runPreflightByMode,
|
|
174
171
|
runExecutionByMode,
|
|
175
172
|
resolveManagedExecutionPreflight,
|
|
176
|
-
applyExecutionFallbackHints,
|
|
177
173
|
performSelfBroadcast,
|
|
178
174
|
executeManagedWalletRun,
|
|
179
175
|
executeSelfRun,
|
package/src/runtime/main.mjs
CHANGED
|
@@ -16,7 +16,6 @@ import { createInitialRuntimeContext, applyFlagsToRuntimeContext } from './conte
|
|
|
16
16
|
import { createProviderHints } from './providerHints/dispatch.mjs';
|
|
17
17
|
import { createRuntimeHandlers } from './handlers.mjs';
|
|
18
18
|
import { isCommanderCliError } from './errors.mjs';
|
|
19
|
-
import { createOnboardingHints } from './onboardingHints.mjs';
|
|
20
19
|
import { walletControlErrorFields } from './execution/walletControl.mjs';
|
|
21
20
|
import { createAgentExtract } from './agentExtract.mjs';
|
|
22
21
|
import { createAuthHelpers } from './authHelpers.mjs';
|
|
@@ -25,7 +24,6 @@ import { createExecutionLanes } from './execution/lanes.mjs';
|
|
|
25
24
|
import { createExecutionPayloads } from './execution/payloads.mjs';
|
|
26
25
|
import { createExecutionRunners } from './execution/runners/createExecutionRunners.mjs';
|
|
27
26
|
import { createActionSelection } from './actions/selection.mjs';
|
|
28
|
-
import { createBitfinexProbe } from './bitfinexProbe.mjs';
|
|
29
27
|
import { createOwsSigning } from './owsSigning.mjs';
|
|
30
28
|
import { enforceCatalogActionParams } from './validation/actionParamsGate.mjs';
|
|
31
29
|
|
|
@@ -66,7 +64,6 @@ export function createThirdfyAgentRuntime() {
|
|
|
66
64
|
providerHints;
|
|
67
65
|
|
|
68
66
|
const { extractAgentApiKey, extractAgentKey, maskSecret } = createAgentExtract();
|
|
69
|
-
const onboarding = createOnboardingHints();
|
|
70
67
|
const amounts = createExecutionAmounts();
|
|
71
68
|
const lanes = createExecutionLanes();
|
|
72
69
|
const auth = createAuthHelpers();
|
|
@@ -82,7 +79,6 @@ export function createThirdfyAgentRuntime() {
|
|
|
82
79
|
const catalogActionParamsGate = (resolved, flags) =>
|
|
83
80
|
enforceCatalogActionParams(resolved, flags, amounts.getPreparedParams);
|
|
84
81
|
const ows = createOwsSigning();
|
|
85
|
-
const bitfinex = createBitfinexProbe();
|
|
86
82
|
const runners = createExecutionRunners({
|
|
87
83
|
buildIntentPayload: payloads.buildIntentPayload,
|
|
88
84
|
buildBuildTxPayload: payloads.buildBuildTxPayload,
|
|
@@ -92,8 +88,6 @@ export function createThirdfyAgentRuntime() {
|
|
|
92
88
|
resolveTokenInForFunding: payloads.resolveTokenInForFunding,
|
|
93
89
|
shouldUseBuildTxPreflight: lanes.shouldUseBuildTxPreflight,
|
|
94
90
|
shouldUseBuildTxExecution: lanes.shouldUseBuildTxExecution,
|
|
95
|
-
buildBitfinexOnboardingCommand: onboarding.buildBitfinexOnboardingCommand,
|
|
96
|
-
inferBitfinexPermissionProfileFromAction: onboarding.inferBitfinexPermissionProfileFromAction,
|
|
97
91
|
resolveAgentApiKey: payloads.resolveAgentApiKey,
|
|
98
92
|
resolveAgentKey: payloads.resolveAgentKey,
|
|
99
93
|
loadEthers: ows.loadEthers,
|
|
@@ -111,7 +105,6 @@ export function createThirdfyAgentRuntime() {
|
|
|
111
105
|
maskSecret,
|
|
112
106
|
getAuthToken: auth.getAuthToken,
|
|
113
107
|
buildOwnerAuthHeaders: auth.buildOwnerAuthHeaders,
|
|
114
|
-
probeBitfinexPermissions: bitfinex.probeBitfinexPermissions,
|
|
115
108
|
applyActionFilters: actions.applyActionFilters,
|
|
116
109
|
getProviderHintForActions,
|
|
117
110
|
resolveChainSupport: lanes.resolveChainSupport,
|
|
@@ -127,7 +120,6 @@ export function createThirdfyAgentRuntime() {
|
|
|
127
120
|
submitSignedTx: runners.submitSignedTx,
|
|
128
121
|
runPreflightByMode: runners.runPreflightByMode,
|
|
129
122
|
runExecutionByMode: runners.runExecutionByMode,
|
|
130
|
-
applyExecutionFallbackHints: runners.applyExecutionFallbackHints,
|
|
131
123
|
performSelfBroadcast: runners.performSelfBroadcast,
|
|
132
124
|
resolveManagedExecutionPreflight: runners.resolveManagedExecutionPreflight,
|
|
133
125
|
resolveEffectiveUserDid: payloads.resolveEffectiveUserDid,
|
|
@@ -195,7 +187,6 @@ export function createThirdfyAgentRuntime() {
|
|
|
195
187
|
const blockedReason = String(payload.blockedReason || payload.error || '').trim() || undefined;
|
|
196
188
|
const blockedStage = String(payload.blockedStage || '').trim() || undefined;
|
|
197
189
|
const clientCode = String(payload.error || payload.blockedReason || '').trim() || undefined;
|
|
198
|
-
const onboardingHint = onboarding.buildTopLevelOnboardingHint(payload);
|
|
199
190
|
printEnvelope({
|
|
200
191
|
success: false,
|
|
201
192
|
// A missing or malformed flag is INVALID_ARGUMENTS at the top too, not an unhandled error.
|
|
@@ -209,7 +200,6 @@ export function createThirdfyAgentRuntime() {
|
|
|
209
200
|
field: payload.field || null,
|
|
210
201
|
resolvedAction: payload.resolvedAction || null,
|
|
211
202
|
requestedAction: payload.requestedAction || null,
|
|
212
|
-
onboardingHint,
|
|
213
203
|
// errorCode, retryable, retryAfterSeconds, nextAction, hint: as thirdfy-mcp's error envelope.
|
|
214
204
|
...describeCliError(error),
|
|
215
205
|
// API 4.0 wallet-control refusals: walletControl, nextAction, userMessage, retryable, hint.
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import { createRequire } from 'module';
|
|
3
4
|
import os from 'os';
|
|
4
5
|
import path from 'path';
|
|
6
|
+
import { pinnedRpcProvider } from '../core/chainRegistry.mjs';
|
|
5
7
|
import { createCliError } from '../core/envelope.mjs';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
6
10
|
export function createOwsSigning() {
|
|
11
|
+
// ethers is a dependency of this package; resolve it from here, never from the caller's directory.
|
|
7
12
|
async function loadEthers() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} catch {
|
|
13
|
-
// continue
|
|
14
|
-
}
|
|
13
|
+
try {
|
|
14
|
+
return require('ethers');
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error('Could not load the ethers dependency of @thirdfy/agent-cli; reinstall the package');
|
|
15
17
|
}
|
|
16
|
-
throw new Error('Could not load ethers dependency for self-exec');
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function resolveOwsBinary() {
|
|
@@ -62,8 +63,8 @@ function signUnsignedTxWithOws(unsignedHex, walletName, chainId) {
|
|
|
62
63
|
|
|
63
64
|
async function signAndBroadcastWithOws({ txd, chainId, walletAddress, owsWalletName, rpcUrl }) {
|
|
64
65
|
const ethers = await loadEthers();
|
|
65
|
-
const {
|
|
66
|
-
const provider =
|
|
66
|
+
const { Transaction, Signature } = ethers;
|
|
67
|
+
const provider = pinnedRpcProvider(ethers, { rpcUrl, chainId });
|
|
67
68
|
const nonce = await provider.getTransactionCount(walletAddress, 'pending');
|
|
68
69
|
const fee = await provider.getFeeData();
|
|
69
70
|
const txFields = {
|
|
@@ -86,10 +87,10 @@ async function signAndBroadcastWithOws({ txd, chainId, walletAddress, owsWalletN
|
|
|
86
87
|
return { sent };
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
async function readTokenBalanceRaw(rpcUrl, walletAddress, tokenAddress) {
|
|
90
|
+
async function readTokenBalanceRaw(rpcUrl, walletAddress, tokenAddress, chainId) {
|
|
90
91
|
const ethers = await loadEthers();
|
|
91
|
-
const {
|
|
92
|
-
const provider =
|
|
92
|
+
const { Interface } = ethers;
|
|
93
|
+
const provider = pinnedRpcProvider(ethers, { rpcUrl, chainId });
|
|
93
94
|
const erc20 = new Interface(['function balanceOf(address owner) view returns (uint256)']);
|
|
94
95
|
const response = await provider.call({
|
|
95
96
|
to: tokenAddress,
|
|
@@ -99,10 +100,10 @@ async function readTokenBalanceRaw(rpcUrl, walletAddress, tokenAddress) {
|
|
|
99
100
|
return BigInt(decoded[0].toString()).toString();
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
async function readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut) {
|
|
103
|
+
async function readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut, chainId) {
|
|
103
104
|
const [inBal, outBal] = await Promise.all([
|
|
104
|
-
readTokenBalanceRaw(rpcUrl, walletAddress, tokenIn),
|
|
105
|
-
readTokenBalanceRaw(rpcUrl, walletAddress, tokenOut),
|
|
105
|
+
readTokenBalanceRaw(rpcUrl, walletAddress, tokenIn, chainId),
|
|
106
|
+
readTokenBalanceRaw(rpcUrl, walletAddress, tokenOut, chainId),
|
|
106
107
|
]);
|
|
107
108
|
return { tokenIn: inBal, tokenOut: outBal };
|
|
108
109
|
}
|
|
@@ -1,59 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Static provider hints. Centralized
|
|
2
|
+
* Static provider hints. Centralized exchange executors: Hummingbot and Condor.
|
|
3
3
|
* Each entry builds the hint for one provider id; `dispatch.mjs` picks the entry.
|
|
4
4
|
*/
|
|
5
5
|
export const CEX_PROVIDER_HINTS = Object.freeze({
|
|
6
|
-
bitfinex: ({ provider }) => ({
|
|
7
|
-
provider,
|
|
8
|
-
category: 'cex_funding_and_spot',
|
|
9
|
-
canonicalReadAction: 'get_bitfinex_funding_ticker',
|
|
10
|
-
canonicalWriteAction: 'submit_bitfinex_funding_offer',
|
|
11
|
-
readActions: [
|
|
12
|
-
'get_bitfinex_funding_stats_hist',
|
|
13
|
-
'get_bitfinex_funding_ticker',
|
|
14
|
-
'get_bitfinex_funding_book',
|
|
15
|
-
'get_bitfinex_wallets',
|
|
16
|
-
'list_bitfinex_funding_offers',
|
|
17
|
-
'list_bitfinex_funding_offers_hist',
|
|
18
|
-
'list_bitfinex_funding_credits',
|
|
19
|
-
'list_bitfinex_funding_loans',
|
|
20
|
-
'get_bitfinex_funding_info',
|
|
21
|
-
'get_bitfinex_ledgers_hist',
|
|
22
|
-
'get_bitfinex_orders',
|
|
23
|
-
],
|
|
24
|
-
orderManagementActions: [
|
|
25
|
-
'submit_bitfinex_funding_offer',
|
|
26
|
-
'cancel_bitfinex_funding_offer',
|
|
27
|
-
'transfer_bitfinex_between_wallets',
|
|
28
|
-
'get_bitfinex_orders',
|
|
29
|
-
'submit_bitfinex_order',
|
|
30
|
-
'update_bitfinex_order',
|
|
31
|
-
'cancel_bitfinex_order',
|
|
32
|
-
],
|
|
33
|
-
credentialType: 'api_key',
|
|
34
|
-
credentialRef: 'bitfinex:funding-primary',
|
|
35
|
-
credentialUx: 'Bitfinex credentials are owned by Thirdfy API encrypted delegation storage. CLI commands pass credentialRef only; never apiKey/apiSecret.',
|
|
36
|
-
laneCompatibility: 'Use `thirdfy-agent actions --provider bitfinex` for discovery, then `thirdfy-agent run --run-mode thirdfy` with credentialRef in params. The CLI does not store, log, or sign raw exchange credentials.',
|
|
37
|
-
example: 'thirdfy-agent actions --provider bitfinex && thirdfy-agent run --run-mode thirdfy --action submit_bitfinex_funding_offer --params \'{"symbol":"fUSD","amount":"100","rate":"0.0002","period":2,"credentialRef":"bitfinex:funding-primary"}\'',
|
|
38
|
-
credentialProfiles: [
|
|
39
|
-
{
|
|
40
|
-
id: 'bitfinex_funding_v1',
|
|
41
|
-
credentialRef: 'bitfinex:funding-primary',
|
|
42
|
-
required: ['funding.read', 'funding.write', 'wallets.read'],
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: 'bitfinex_spot_v1',
|
|
46
|
-
credentialRef: 'bitfinex:spot-primary',
|
|
47
|
-
required: ['orders.read', 'orders.write', 'wallets.read'],
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
setupBlockers: [
|
|
51
|
-
'CEX_CREDENTIAL_NOT_FOUND',
|
|
52
|
-
'BITFINEX_CREDENTIALS_NOT_IN_THIRDFY_DELEGATION_CONTEXT',
|
|
53
|
-
'BITFINEX_PERMISSION_SCOPE_MISSING',
|
|
54
|
-
'CEX_POLICY_ACTION_NOT_ALLOWED',
|
|
55
|
-
],
|
|
56
|
-
}),
|
|
57
6
|
hummingbot: ({ provider }) => ({
|
|
58
7
|
provider,
|
|
59
8
|
category: 'cex_executor',
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { createHmac } from 'crypto';
|
|
2
|
-
import { DEFAULT_BITFINEX_BASE_URL, BITFINEX_PERMISSION_PROFILES } from '../core/constants.mjs';
|
|
3
|
-
import { createCliError } from '../core/envelope.mjs';
|
|
4
|
-
import { requestJson, safeJsonParse } from '../core/http.mjs';
|
|
5
|
-
export function createBitfinexProbe() {
|
|
6
|
-
function parseBitfinexPermissionRows(rawData) {
|
|
7
|
-
const rows = Array.isArray(rawData) ? rawData : [];
|
|
8
|
-
const parsed = {};
|
|
9
|
-
for (const row of rows) {
|
|
10
|
-
if (!Array.isArray(row) || row.length < 3) continue;
|
|
11
|
-
const scope = String(row[0] || '').trim().toLowerCase();
|
|
12
|
-
if (!scope) continue;
|
|
13
|
-
const read = Number(row[1]) === 1 ? 1 : 0;
|
|
14
|
-
const write = Number(row[2]) === 1 ? 1 : 0;
|
|
15
|
-
parsed[scope] = { read, write };
|
|
16
|
-
}
|
|
17
|
-
return parsed;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getMissingBitfinexScopes(scopePermissions, permissionProfile = 'funding') {
|
|
21
|
-
const profile = BITFINEX_PERMISSION_PROFILES[permissionProfile] || BITFINEX_PERMISSION_PROFILES.funding;
|
|
22
|
-
const missing = [];
|
|
23
|
-
for (const [scope, required] of Object.entries(profile.required)) {
|
|
24
|
-
const actual = scopePermissions[scope] || { read: 0, write: 0 };
|
|
25
|
-
if (actual.read < required.read || actual.write < required.write) {
|
|
26
|
-
missing.push({
|
|
27
|
-
scope,
|
|
28
|
-
required,
|
|
29
|
-
actual,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return missing;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function probeBitfinexPermissions({ apiKey, apiSecret, permissionProfile = 'funding', timeoutMs, baseUrl }) {
|
|
37
|
-
const normalizedBaseUrl = String(baseUrl || DEFAULT_BITFINEX_BASE_URL).trim().replace(/\/+$/, '');
|
|
38
|
-
const apiPath = '/v2/auth/r/permissions';
|
|
39
|
-
const nonce = Date.now().toString();
|
|
40
|
-
const payload = '{}';
|
|
41
|
-
const signature = createHmac('sha384', String(apiSecret || ''))
|
|
42
|
-
.update(`/api${apiPath}${nonce}${payload}`)
|
|
43
|
-
.digest('hex');
|
|
44
|
-
const controller = new AbortController();
|
|
45
|
-
const timer = setTimeout(() => controller.abort(), Number(timeoutMs || DEFAULT_TIMEOUT_MS));
|
|
46
|
-
try {
|
|
47
|
-
const response = await fetch(`${normalizedBaseUrl}${apiPath}`, {
|
|
48
|
-
method: 'POST',
|
|
49
|
-
headers: {
|
|
50
|
-
'content-type': 'application/json',
|
|
51
|
-
'bfx-apikey': String(apiKey || ''),
|
|
52
|
-
'bfx-nonce': nonce,
|
|
53
|
-
'bfx-signature': signature,
|
|
54
|
-
},
|
|
55
|
-
body: payload,
|
|
56
|
-
signal: controller.signal,
|
|
57
|
-
});
|
|
58
|
-
const text = await response.text();
|
|
59
|
-
const parsed = text ? safeJsonParse(text) : {};
|
|
60
|
-
const bitfinexApiError =
|
|
61
|
-
Array.isArray(parsed) && String(parsed[0] || '').trim().toLowerCase() === 'error'
|
|
62
|
-
? {
|
|
63
|
-
code: Number(parsed[1]) || null,
|
|
64
|
-
message: String(parsed[2] || 'Unknown Bitfinex API error'),
|
|
65
|
-
}
|
|
66
|
-
: parsed && typeof parsed === 'object' && String(parsed.event || '').trim().toLowerCase() === 'error'
|
|
67
|
-
? {
|
|
68
|
-
code: Number(parsed.code) || null,
|
|
69
|
-
message: String(parsed.msg || parsed.message || 'Unknown Bitfinex API error'),
|
|
70
|
-
}
|
|
71
|
-
: null;
|
|
72
|
-
if (!response.ok) {
|
|
73
|
-
throw createCliError(
|
|
74
|
-
'BITFINEX_PERMISSIONS_PROBE_FAILED',
|
|
75
|
-
`Bitfinex permissions probe failed with HTTP ${response.status}. Verify API key/secret and key validity before onboarding.`,
|
|
76
|
-
{ statusCode: response.status }
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
if (bitfinexApiError) {
|
|
80
|
-
throw createCliError(
|
|
81
|
-
'BITFINEX_PERMISSIONS_PROBE_FAILED',
|
|
82
|
-
`Bitfinex permissions probe failed: ${bitfinexApiError.message}. Verify API key/secret and key validity before onboarding.`,
|
|
83
|
-
{ statusCode: response.status, bitfinexApiError }
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
const scopePermissions = parseBitfinexPermissionRows(parsed);
|
|
87
|
-
const missingScopes = getMissingBitfinexScopes(scopePermissions, permissionProfile);
|
|
88
|
-
if (missingScopes.length > 0) {
|
|
89
|
-
throw createCliError(
|
|
90
|
-
'BITFINEX_PERMISSION_SCOPE_MISSING',
|
|
91
|
-
`Bitfinex key is missing required scopes (${missingScopes
|
|
92
|
-
.map((entry) => entry.scope)
|
|
93
|
-
.join(', ')}). Enable the ${permissionProfile} profile scopes and retry onboarding.`,
|
|
94
|
-
{ missingScopes, scopePermissions, permissionProfile }
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
return {
|
|
98
|
-
venue: 'bitfinex',
|
|
99
|
-
permissionProfile,
|
|
100
|
-
checked: true,
|
|
101
|
-
scopes: scopePermissions,
|
|
102
|
-
missingScopes: [],
|
|
103
|
-
};
|
|
104
|
-
} catch (error) {
|
|
105
|
-
if (error?.payload) throw error;
|
|
106
|
-
throw createCliError(
|
|
107
|
-
'BITFINEX_PERMISSIONS_PROBE_FAILED',
|
|
108
|
-
`Could not verify Bitfinex key permissions. ${String(error?.message || 'Unknown probe error')}`,
|
|
109
|
-
{}
|
|
110
|
-
);
|
|
111
|
-
} finally {
|
|
112
|
-
clearTimeout(timer);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
parseBitfinexPermissionRows,
|
|
118
|
-
getMissingBitfinexScopes,
|
|
119
|
-
probeBitfinexPermissions,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/** Hints added to a failed execution answer (Bitfinex onboarding and similar next steps). */
|
|
2
|
-
export function createFallbackHints(deps) {
|
|
3
|
-
const {
|
|
4
|
-
buildBitfinexOnboardingCommand,
|
|
5
|
-
inferBitfinexPermissionProfileFromAction,
|
|
6
|
-
} = deps;
|
|
7
|
-
|
|
8
|
-
function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedAction }) {
|
|
9
|
-
if (!normalized || typeof normalized !== 'object') return normalized;
|
|
10
|
-
const next = { ...normalized };
|
|
11
|
-
const reasons = new Set();
|
|
12
|
-
const topBlockedReason = String(next.blockedReason || '').trim();
|
|
13
|
-
if (topBlockedReason) reasons.add(topBlockedReason);
|
|
14
|
-
const blockedByReason = next.blockedByReason && typeof next.blockedByReason === 'object' ? next.blockedByReason : {};
|
|
15
|
-
for (const key of Object.keys(blockedByReason)) {
|
|
16
|
-
if (String(key || '').trim()) reasons.add(String(key).trim());
|
|
17
|
-
}
|
|
18
|
-
const resolvedActionLower = String(resolvedAction || '').trim().toLowerCase();
|
|
19
|
-
const looksBitfinexAction = resolvedActionLower.includes('bitfinex');
|
|
20
|
-
const permissionProfile = inferBitfinexPermissionProfileFromAction(resolvedActionLower);
|
|
21
|
-
const cexCredentialMissingSignals = new Set([
|
|
22
|
-
'CEX_CREDENTIAL_NOT_FOUND',
|
|
23
|
-
'BITFINEX_CREDENTIALS_NOT_IN_CONTEXT',
|
|
24
|
-
'BITFINEX_CREDENTIALS_NOT_IN_THIRDFY_DELEGATION_CONTEXT',
|
|
25
|
-
'BITFINEX_ENV_CREDENTIALS_NOT_CONFIGURED',
|
|
26
|
-
]);
|
|
27
|
-
const hasMissingCredentialSignal = Array.from(reasons).some((code) => cexCredentialMissingSignals.has(code));
|
|
28
|
-
if (looksBitfinexAction && hasMissingCredentialSignal) {
|
|
29
|
-
next.onboardingHint = {
|
|
30
|
-
type: 'bitfinex_credentials',
|
|
31
|
-
message:
|
|
32
|
-
'Bitfinex credentials are missing for this delegated account. Onboard credentials first, then retry the same command.',
|
|
33
|
-
command: buildBitfinexOnboardingCommand(permissionProfile),
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
if (
|
|
37
|
-
looksBitfinexAction &&
|
|
38
|
-
runMode !== 'thirdfy' &&
|
|
39
|
-
runMode !== 'agent_wallet' &&
|
|
40
|
-
(topBlockedReason === 'SELF_MODE_REQUIRES_BUILD_TX' || topBlockedReason === 'HYBRID_MODE_REQUIRES_BUILD_TX')
|
|
41
|
-
) {
|
|
42
|
-
next.onboardingHint = {
|
|
43
|
-
...(next.onboardingHint || {}),
|
|
44
|
-
runMode: 'thirdfy',
|
|
45
|
-
runModeMessage: 'Bitfinex actions require delegated execute-intent rail. Re-run with --run-mode thirdfy.',
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
return next;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
applyExecutionFallbackHints,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { BITFINEX_PERMISSION_PROFILES } from '../core/constants.mjs';
|
|
2
|
-
export function createOnboardingHints() {
|
|
3
|
-
function buildBitfinexOnboardingCommand(permissionProfile = 'funding') {
|
|
4
|
-
const profile = BITFINEX_PERMISSION_PROFILES[permissionProfile] || BITFINEX_PERMISSION_PROFILES.funding;
|
|
5
|
-
return [
|
|
6
|
-
'thirdfy-agent credentials upsert',
|
|
7
|
-
'--auth-token "$THIRDFY_AUTH_TOKEN"',
|
|
8
|
-
'--venue bitfinex',
|
|
9
|
-
`--permission-profile ${profile.id}`,
|
|
10
|
-
`--credential-ref ${profile.credentialRef}`,
|
|
11
|
-
'--api-key "<BITFINEX_API_KEY>"',
|
|
12
|
-
'--api-secret "<BITFINEX_API_SECRET>"',
|
|
13
|
-
'--verify-permissions',
|
|
14
|
-
'--json',
|
|
15
|
-
].join(' ');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function inferBitfinexPermissionProfileFromAction(actionName) {
|
|
19
|
-
const action = String(actionName || '')
|
|
20
|
-
.trim()
|
|
21
|
-
.toLowerCase()
|
|
22
|
-
.replace(/[_\s]+/g, '-');
|
|
23
|
-
if (!action.includes('bitfinex')) return 'funding';
|
|
24
|
-
return action.includes('bitfinex-order') ? 'spot' : 'funding';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function buildTopLevelOnboardingHint(payload) {
|
|
28
|
-
const reason = String(payload?.blockedReason || payload?.error || '').trim();
|
|
29
|
-
const permissionProfile = inferBitfinexPermissionProfileFromAction(
|
|
30
|
-
payload?.resolvedAction || payload?.action || payload?.requestedAction
|
|
31
|
-
);
|
|
32
|
-
if (!reason) return null;
|
|
33
|
-
if (
|
|
34
|
-
[
|
|
35
|
-
'CEX_CREDENTIAL_NOT_FOUND',
|
|
36
|
-
'BITFINEX_CREDENTIALS_NOT_IN_CONTEXT',
|
|
37
|
-
'BITFINEX_CREDENTIALS_NOT_IN_THIRDFY_DELEGATION_CONTEXT',
|
|
38
|
-
'BITFINEX_ENV_CREDENTIALS_NOT_CONFIGURED',
|
|
39
|
-
].includes(reason)
|
|
40
|
-
) {
|
|
41
|
-
return {
|
|
42
|
-
type: 'bitfinex_credentials',
|
|
43
|
-
command: buildBitfinexOnboardingCommand(permissionProfile),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
buildBitfinexOnboardingCommand,
|
|
51
|
-
inferBitfinexPermissionProfileFromAction,
|
|
52
|
-
buildTopLevelOnboardingHint,
|
|
53
|
-
};
|
|
54
|
-
}
|