Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 22 additions & 25 deletions src/commands/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { formatChainId, formatChainIds } from "../lib/chains";
import { c } from "../lib/color";
import { openBrowser } from "../lib/browser";
import { selectOption, prompt } from "../lib/prompt";
import { withApprovalGate } from "../lib/walletGate";
import qrcode from "qrcode-terminal";

export function registerWalletCommands(program: Command): void {
Expand All @@ -37,10 +38,8 @@ export function registerWalletCommands(program: Command): void {
.action(async (opts, cmd) => {
const json = isJson(cmd);
try {
const provider = await createProviderAdapter();
const signature = await provider.signMessage(
Number(opts.chainId),
opts.message
const signature = await withApprovalGate((provider) =>
provider.signMessage(Number(opts.chainId), opts.message)
);
outputResult(json, { signature });
} catch (err) {
Expand Down Expand Up @@ -82,10 +81,8 @@ export function registerWalletCommands(program: Command): void {
);
}

const provider = await createProviderAdapter();
const signature = await provider.signTypedData(
Number(opts.chainId),
typedData
const signature = await withApprovalGate((provider) =>
provider.signTypedData(Number(opts.chainId), typedData)
);
outputResult(json, { signature });
} catch (err) {
Expand All @@ -112,16 +109,6 @@ export function registerWalletCommands(program: Command): void {
);
}

const provider = await createProviderAdapter();
const supportedChainIds = await provider.getSupportedChainIds();
if (!supportedChainIds.includes(chainId)) {
throw new CliError(
`Unsupported chain ID: ${formatChainId(chainId)}`,
"VALIDATION_ERROR",
`Supported chains: ${formatChainIds(supportedChainIds)}`
);
}

if (!isAddress(opts.to)) {
throw new CliError(
`Invalid --to address: ${opts.to}`,
Expand Down Expand Up @@ -151,10 +138,20 @@ export function registerWalletCommands(program: Command): void {
}
}

const transactionHash = await provider.sendTransaction(chainId, {
to: opts.to,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant getSupportedChainIds call in send-transaction path

Low Severity

In the send-transaction command, provider.getSupportedChainIds() is called twice on the same provider instance within a single withApprovalGate invocation — once inside createSseTransport (to populate providerSupportedChainIds in the transport context) and again in the callback at line 142 for chain validation. Since both calls happen on the same provider, the second call is redundant work (likely an additional network round-trip). The withApprovalGate API could expose the already-fetched chain IDs to avoid this duplication.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 23a4560. Configure here.

...(opts.data !== undefined ? { data: opts.data } : {}),
...(value !== undefined ? { value } : {}),
const transactionHash = await withApprovalGate(async (provider) => {
const supportedChainIds = await provider.getSupportedChainIds();
if (!supportedChainIds.includes(chainId)) {
throw new CliError(
`Unsupported chain ID: ${formatChainId(chainId)}`,
"VALIDATION_ERROR",
`Supported chains: ${formatChainIds(supportedChainIds)}`
);
}
return provider.sendTransaction(chainId, {
to: opts.to,
...(opts.data !== undefined ? { data: opts.data } : {}),
...(value !== undefined ? { value } : {}),
});
});
outputResult(json, { transactionHash });
} catch (err) {
Expand Down Expand Up @@ -368,9 +365,9 @@ export function registerWalletCommands(program: Command): void {
if (!json && isTTY()) {
process.stdout.write(" Signing wallet verification...");
}
signature = await provider.signMessage(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Topup command creates redundant expensive provider adapter

Low Severity

In the topup command, createProviderAdapter() is called at line 285 for chain validation, and then withApprovalGate at line 369 internally calls createProviderAdapter() a second time (plus creates an SSE transport). Before this PR, the single provider was reused for both validation and signing. Each createProviderFromConfig call potentially involves API calls (wallet ID lookup, builder code fetch) and creates a full PrivyAlchemyEvmProviderAdapter, making this a meaningful duplication of expensive work in the card+challenge flow.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 14d8f65. Configure here.

chainId,
initResult.data.challenge
const challenge = initResult.data.challenge;
signature = await withApprovalGate((p) =>
p.signMessage(chainId, challenge)
);
if (!json && isTTY()) {
console.log(` ${c.green("✓")}`);
Expand Down
30 changes: 29 additions & 1 deletion src/lib/agentFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {
SseTransport,
AcpApiClient,
} from "@virtuals-protocol/acp-node-v2";
import type { IEvmProviderAdapter } from "@virtuals-protocol/acp-node-v2";
import type {
IEvmProviderAdapter,
SupportedStreams,
} from "@virtuals-protocol/acp-node-v2";
import {
getBuilderCode,
getActiveWallet,
Expand Down Expand Up @@ -164,6 +167,31 @@ export async function createProviderAdapter(): Promise<IEvmProviderAdapter> {
return createProviderFromConfig(chains, serverUrl, privyAppId);
}

export async function createSseTransport(
provider: IEvmProviderAdapter,
streams: SupportedStreams[]
): Promise<SseTransport> {
const isTestnet = process.env.IS_TESTNET === "true";
const serverUrl = isTestnet ? ACP_TESTNET_SERVER_URL : ACP_SERVER_URL;
const [agentAddress, providerSupportedChainIds] = await Promise.all([
provider.getAddress(),
provider.getSupportedChainIds(),
]);

const ctx = {
agentAddress,
contractAddresses: ACP_CONTRACT_ADDRESSES,
providerSupportedChainIds,
signTypedData: (chainId, typedData) =>
provider.signTypedData(chainId, typedData),
} as Parameters<SseTransport["setContext"]>[0];

const transport = new SseTransport({ serverUrl });
transport.setContext(ctx);
await transport.connect(undefined, streams);
return transport;
}

export function getWalletAddress(): string {
const addr = getActiveWallet();
if (!addr) {
Expand Down
17 changes: 17 additions & 0 deletions src/lib/walletGate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
STREAMS,
type IEvmProviderAdapter,
} from "@virtuals-protocol/acp-node-v2";
import { createProviderAdapter, createSseTransport } from "./agentFactory";

export async function withApprovalGate<T>(
fn: (provider: IEvmProviderAdapter) => Promise<T>
): Promise<T> {
const provider = await createProviderAdapter();
const transport = await createSseTransport(provider, [STREAMS.WALLET]);
try {
return await fn(provider);
} finally {
void Promise.resolve(transport.disconnect()).catch(() => {});
}
}