Skip to content

Commit eb9184d

Browse files
authored
feat: wallet provider system (#93)
1 parent 27de314 commit eb9184d

File tree

15 files changed

+285
-15
lines changed

15 files changed

+285
-15
lines changed

packages/use-agently/src/cli.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ Quick Reference:
6969
HTTP requests with x402 payment support:
7070
web get|post|put|patch|delete <url> [-d <body>] [-H <header>] [-v] [--pay]
7171
72-
Manage wallet spend limits:
72+
Manage wallet providers and spend limits:
73+
wallet providers
74+
wallet set <provider>
7375
wallet spend
7476
wallet spend set-max <value>
7577

packages/use-agently/src/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
getChainConfigByNetwork,
88
type PaymentRequirementsInfo,
99
type unstable_Client,
10-
loadWallet,
1110
} from "@use-agently/sdk";
1211
import { getConfigOrThrow, getMaxSpendPerCall } from "./config";
12+
import { resolveWallet } from "./wallet";
1313
import pkg from "../package.json" with { type: "json" };
1414
import { createClient } from "@use-agently/sdk/client";
1515

@@ -119,7 +119,7 @@ export function createSpendLimitedFetch(baseFetch: typeof fetch, maxSpendPerCall
119119
export async function resolveFetch(pay?: boolean): Promise<typeof fetch> {
120120
if (pay) {
121121
const config = await getConfigOrThrow();
122-
const wallet = loadWallet(config.wallet);
122+
const wallet = await resolveWallet(config);
123123
const maxSpend = getMaxSpendPerCall(config);
124124
const limitedFetch = createSpendLimitedFetch(clientFetch, maxSpend);
125125
return sdkCreatePaymentFetch(wallet, limitedFetch) as typeof fetch;

packages/use-agently/src/commands/balance.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ describe("balance command", () => {
4343
currency: "USDC",
4444
network: "Base",
4545
balance: expect.any(String),
46+
provider: "local",
47+
otherProviders: [],
4648
});
4749
expect(Number(parsed.balance)).toBeGreaterThan(0);
4850
});
Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Command } from "commander";
2-
import { output } from "../output";
3-
import { loadWallet, getBalance } from "@use-agently/sdk";
4-
import { getConfigOrThrow } from "../config";
2+
import { getOutputFormat, outputJson, outputTuiKeyValue } from "../output";
3+
import { getBalance } from "@use-agently/sdk";
4+
import { getConfigOrThrow, getActiveProvider } from "../config";
5+
import { resolveWallet } from "../wallet";
6+
import { detectProviders } from "../providers";
57

68
export const balanceCommand = new Command("balance")
79
.description("Check wallet balance on-chain")
@@ -10,7 +12,37 @@ export const balanceCommand = new Command("balance")
1012
.addHelpText("after", "\nExamples:\n use-agently balance")
1113
.action(async (options: { rpc?: string }, command: Command) => {
1214
const config = await getConfigOrThrow();
13-
const wallet = loadWallet(config.wallet);
15+
const wallet = await resolveWallet(config);
1416
const result = await getBalance(wallet.address, { rpc: options.rpc });
15-
output(command, result);
17+
const activeProvider = getActiveProvider(config);
18+
19+
const detected = await detectProviders(activeProvider);
20+
const otherProviders = detected.filter((p) => p.installed && !p.active);
21+
22+
const format = getOutputFormat(command);
23+
24+
if (format === "json") {
25+
outputJson({
26+
...result,
27+
provider: activeProvider,
28+
otherProviders: otherProviders.map((p) => ({
29+
type: p.type,
30+
name: p.name,
31+
address: p.address,
32+
switchCommand: `use-agently wallet set ${p.type}`,
33+
})),
34+
});
35+
} else {
36+
outputTuiKeyValue({ ...result, provider: activeProvider });
37+
38+
if (otherProviders.length > 0) {
39+
console.log("");
40+
console.log("Other wallets detected:");
41+
for (const p of otherProviders) {
42+
const addr = p.address ? ` ${p.address}` : "";
43+
console.log(` ${p.name}${addr}`);
44+
}
45+
console.log("\nSwitch provider: use-agently wallet set <provider>");
46+
}
47+
}
1648
});

packages/use-agently/src/commands/init.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ mock.module("../config.js", () => ({
1414
getConfigOrThrow: async () => {
1515
throw new Error("No wallet configured.");
1616
},
17+
getActiveProvider: () => "local",
18+
}));
19+
20+
mock.module("../wallet.js", () => ({
21+
resolveWallet: async (config: any) => {
22+
const { loadWallet } = await import("@use-agently/sdk");
23+
return loadWallet(config.wallet);
24+
},
1725
}));
1826

1927
const sdk = await import("@use-agently/sdk");

packages/use-agently/src/commands/update.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ mock.module("../config.js", () => ({
2020
getConfigOrThrow: async () => {
2121
throw new Error("No wallet configured.");
2222
},
23+
getActiveProvider: () => "local",
24+
}));
25+
26+
mock.module("../wallet.js", () => ({
27+
resolveWallet: async (config: any) => {
28+
const { loadWallet } = await import("@use-agently/sdk");
29+
return loadWallet(config.wallet);
30+
},
2331
}));
2432

2533
mock.module("node:child_process", () => ({

packages/use-agently/src/commands/wallet.ts

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,82 @@
11
import { Command } from "commander";
2-
import { getConfigOrThrow, saveConfig, getMaxSpendPerCall } from "../config";
3-
import { output } from "../output";
2+
import { getConfigOrThrow, saveConfig, getMaxSpendPerCall, loadConfig, getActiveProvider } from "../config";
3+
import { output, getOutputFormat, Table, boldBlue } from "../output";
4+
import { detectProviders, getProvider } from "../providers";
45

56
export const walletCommand = new Command("wallet")
6-
.description("Manage wallet settings (spend limits)")
7-
.addHelpText("after", "\nExamples:\n use-agently wallet spend\n use-agently wallet spend set-max 0.5")
7+
.description("Manage wallet settings (providers, spend limits)")
8+
.addHelpText(
9+
"after",
10+
"\nExamples:\n use-agently wallet providers\n use-agently wallet set agentcash\n use-agently wallet spend\n use-agently wallet spend set-max 0.5",
11+
)
812
.action(function () {
913
(this as Command).outputHelp();
1014
});
1115

16+
// ─── wallet providers ─────────────────────────────────────────────────────────
17+
18+
const providersCommand = new Command("providers")
19+
.description("List detected wallet providers")
20+
.showHelpAfterError(true)
21+
.addHelpText("after", "\nExamples:\n use-agently wallet providers\n use-agently wallet providers -o json")
22+
.action(async (_options: unknown, command: Command) => {
23+
const config = await loadConfig();
24+
const activeProvider = config ? getActiveProvider(config) : "local";
25+
const detected = await detectProviders(activeProvider);
26+
const format = getOutputFormat(command);
27+
28+
if (format === "json") {
29+
for (const p of detected) {
30+
console.log(JSON.stringify(p));
31+
}
32+
} else {
33+
const table = new Table({ head: ["Provider", "Address", "Status"] });
34+
for (const p of detected) {
35+
const status = p.active ? "active" : p.installed ? "installed" : "not installed";
36+
const name = p.active ? boldBlue(p.name) : p.name;
37+
table.push([name, p.address ?? "—", status]);
38+
}
39+
console.log(table.toString());
40+
console.log("\nSwitch provider: use-agently wallet set <provider>");
41+
}
42+
});
43+
44+
// ─── wallet set ───────────────────────────────────────────────────────────────
45+
46+
const setCommand = new Command("set")
47+
.description("Set the active wallet provider")
48+
.argument("<provider>", 'Provider type (e.g. "agentcash", "local")')
49+
.showHelpAfterError(true)
50+
.addHelpText("after", "\nExamples:\n use-agently wallet set agentcash\n use-agently wallet set local")
51+
.action(async (providerType: string, _options: unknown, command: Command) => {
52+
const provider = getProvider(providerType);
53+
if (!provider) {
54+
const detected = await detectProviders();
55+
const available = detected.map((p) => p.type).join(", ");
56+
throw new Error(`Unknown provider "${providerType}". Available: ${available}`);
57+
}
58+
59+
const { installed } = await provider.detect();
60+
if (!installed) {
61+
throw new Error(`Provider "${providerType}" (${provider.name}) is not installed.`);
62+
}
63+
64+
let config = await loadConfig();
65+
if (!config) {
66+
config = { wallet: { type: "none" } };
67+
}
68+
69+
(config.wallet as Record<string, unknown>).provider = providerType;
70+
await saveConfig(config);
71+
72+
const { address } = await provider.detect();
73+
output(command, {
74+
provider: providerType,
75+
address,
76+
message: `Switched to ${provider.name} wallet`,
77+
});
78+
});
79+
1280
// ─── wallet spend ────────────────────────────────────────────────────────────
1381

1482
const spendCommand = new Command("spend")
@@ -39,4 +107,6 @@ const setMaxCommand = new Command("set-max")
39107
});
40108

41109
spendCommand.addCommand(setMaxCommand);
110+
walletCommand.addCommand(providersCommand);
111+
walletCommand.addCommand(setCommand);
42112
walletCommand.addCommand(spendCommand);

packages/use-agently/src/commands/whoami.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe("whoami command", () => {
1414
expect(out.json).toEqual({
1515
namespace: "eip155",
1616
address: TEST_ADDRESS,
17+
provider: "local",
1718
});
1819
});
1920

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { Command } from "commander";
22
import { output } from "../output";
3-
import { loadWallet } from "@use-agently/sdk";
4-
import { getConfigOrThrow } from "../config";
3+
import { getConfigOrThrow, getActiveProvider } from "../config";
4+
import { resolveWallet } from "../wallet";
55

66
export const whoamiCommand = new Command("whoami")
77
.description("Show current wallet info")
88
.showHelpAfterError(true)
99
.addHelpText("after", "\nExamples:\n use-agently whoami")
1010
.action(async (_options: Record<string, never>, command: Command) => {
1111
const config = await getConfigOrThrow();
12-
const wallet = loadWallet(config.wallet);
12+
const wallet = await resolveWallet(config);
13+
const provider = getActiveProvider(config);
1314

1415
output(command, {
1516
namespace: "eip155",
1617
address: wallet.address,
18+
provider,
1719
});
1820
});

packages/use-agently/src/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,8 @@ export async function getConfigOrThrow(): Promise<Config> {
9696
}
9797
return config;
9898
}
99+
100+
export function getActiveProvider(config: Config): string {
101+
const provider = (config.wallet as Record<string, unknown>)?.provider;
102+
return typeof provider === "string" && provider !== "" ? provider : "local";
103+
}

0 commit comments

Comments
 (0)