diff --git a/coinfello/SKILL.md b/coinfello/SKILL.md index 7d67f0d..bbd12ee 100644 --- a/coinfello/SKILL.md +++ b/coinfello/SKILL.md @@ -1,6 +1,6 @@ --- name: coinfello -description: 'Interact with CoinFello using the openclaw CLI to create MetaMask smart accounts, sign in with SIWE, manage delegations, send prompts with server-driven ERC-20 token subdelegations, and check transaction status. Use when the user wants to send crypto transactions via natural language prompts, manage smart account delegations, or check CoinFello transaction results.' +description: 'Interact with CoinFello using the @coinfello/agent-cli to create MetaMask smart accounts, sign in with SIWE, manage delegations, send prompts with server-driven ERC-20 token subdelegations, and check transaction status. Use when the user wants to send crypto transactions via natural language prompts, manage smart account delegations, or check CoinFello transaction results.' compatibility: Requires Node.js 20+ and pnpm. metadata: { @@ -11,7 +11,7 @@ metadata: # CoinFello CLI Skill -Use the `openclaw` CLI to interact with CoinFello through MetaMask Smart Accounts. The CLI handles smart account creation, SIWE authentication, delegation management, prompt-based transactions, and transaction status checks. +Use the `npx @coinfello/agent-cli` CLI to interact with CoinFello through MetaMask Smart Accounts. The CLI handles smart account creation, SIWE authentication, delegation management, prompt-based transactions, and transaction status checks. ## Prerequisites @@ -19,22 +19,22 @@ Use the `openclaw` CLI to interact with CoinFello through MetaMask Smart Account - pnpm package manager - Build the CLI before first use: `pnpm build` -The CLI binary is available at `./dist/index.js` after building, or as `openclaw` if installed globally. +The CLI is available via `npx @coinfello/agent-cli`. ## Quick Start ```bash # 1. Create a smart account on a chain (generates a new private key automatically) -openclaw create_account sepolia +npx @coinfello/agent-cli create_account sepolia # 2. Sign in to CoinFello with your smart account (SIWE) -openclaw sign_in +npx @coinfello/agent-cli sign_in # 3. Send a natural language prompt — the server will request a delegation if needed -openclaw send_prompt "send 5 USDC to 0xRecipient..." +npx @coinfello/agent-cli send_prompt "send 5 USDC to 0xRecipient..." # 4. Check transaction status -openclaw get_transaction_status +npx @coinfello/agent-cli get_transaction_status ``` ## Commands @@ -44,7 +44,7 @@ openclaw get_transaction_status Creates a MetaMask Hybrid smart account with an auto-generated private key and saves it to local config. ```bash -openclaw create_account +npx @coinfello/agent-cli create_account ``` - `` — A viem chain name: `sepolia`, `mainnet`, `polygon`, `arbitrum`, `optimism`, `base`, etc. @@ -57,7 +57,7 @@ openclaw create_account Displays the current smart account address from local config. ```bash -openclaw get_account +npx @coinfello/agent-cli get_account ``` - Prints the stored `smart_account_address` @@ -68,7 +68,7 @@ openclaw get_account Authenticates with CoinFello using Sign-In with Ethereum (SIWE) and your smart account. Saves the session token to local config. ```bash -openclaw sign_in +npx @coinfello/agent-cli sign_in ``` - Signs in using the private key stored in config @@ -78,27 +78,22 @@ openclaw sign_in ### set_delegation -Stores a signed parent delegation (JSON) in local config for use with redelegation flows. +Stores a signed parent delegation (JSON) in local config. ```bash -openclaw set_delegation '' +npx @coinfello/agent-cli set_delegation '' ``` - `` — A JSON string representing a `Delegation` object from MetaMask Smart Accounts Kit -- Only needed if you plan to use `--use-redelegation` with `send_prompt` ### send_prompt Sends a natural language prompt to CoinFello. If the server requires a delegation to execute the action, the CLI creates and signs a subdelegation automatically based on the server's requested scope and chain. ```bash -openclaw send_prompt "" [--use-redelegation] +npx @coinfello/agent-cli send_prompt "" ``` -**Optional:** - -- `--use-redelegation` — Create a redelegation from a stored parent delegation instead of a fresh subdelegation (requires `set_delegation` first) - **What happens internally:** 1. Fetches available agents from `/api/v1/automation/coinfello-agents` and sends the prompt to CoinFello's conversation endpoint @@ -117,7 +112,7 @@ openclaw send_prompt "" [--use-redelegation] Checks the status of a previously submitted transaction. ```bash -openclaw get_transaction_status +npx @coinfello/agent-cli get_transaction_status ``` - Returns a JSON object with the current transaction status @@ -128,16 +123,16 @@ openclaw get_transaction_status ```bash # Create account if not already done -openclaw create_account sepolia +npx @coinfello/agent-cli create_account sepolia # Sign in (required for delegation flows) -openclaw sign_in +npx @coinfello/agent-cli sign_in # Send a natural language prompt — delegation is handled automatically -openclaw send_prompt "send 5 USDC to 0xRecipient..." +npx @coinfello/agent-cli send_prompt "send 5 USDC to 0xRecipient..." # Check the result -openclaw get_transaction_status +npx @coinfello/agent-cli get_transaction_status ``` ### Read-Only Prompt @@ -145,19 +140,7 @@ openclaw get_transaction_status Some prompts don't require a transaction. The CLI detects this automatically and just prints the response. ```bash -openclaw send_prompt "what is the chain ID for Base?" -``` - -### With Redelegation - -Use this when you have a parent delegation from another delegator and want to create a subdelegation chain. - -```bash -# Store the parent delegation -openclaw set_delegation '{"delegate":"0x...","delegator":"0x...","authority":"0x...","caveats":[],"salt":"0x...","signature":"0x..."}' - -# Send with redelegation -openclaw send_prompt "swap tokens" --use-redelegation +npx @coinfello/agent-cli send_prompt "what is the chain ID for Base?" ``` ## Edge Cases @@ -165,7 +148,6 @@ openclaw send_prompt "swap tokens" --use-redelegation - **No smart account**: Run `create_account` before `send_prompt`. The CLI checks for a saved private key and address in config. - **Not signed in**: Run `sign_in` before `send_prompt` if the server requires authentication. - **Invalid chain name**: The CLI throws an error listing valid viem chain names. -- **Missing parent delegation with --use-redelegation**: The CLI exits with an error. Run `set_delegation` first. - **Read-only response**: If the server returns a text response with no transaction, the CLI prints it and exits without creating a delegation. ## Reference diff --git a/coinfello/references/REFERENCE.md b/coinfello/references/REFERENCE.md index d83dbd2..0f1e3b5 100644 --- a/coinfello/references/REFERENCE.md +++ b/coinfello/references/REFERENCE.md @@ -29,14 +29,14 @@ Created automatically by `create_account`. Schema: | `smart_account_address` | `string` | `create_account` | Counterfactual address of the smart account | | `chain` | `string` | `create_account` | viem chain name used for account creation | | `session_token` | `string` | `sign_in` | SIWE session token for authenticated API calls | -| `delegation` | `object` | `set_delegation` | Optional parent delegation for redelegation | +| `delegation` | `object` | `set_delegation` | Optional stored delegation | ## Command Reference -### openclaw create_account +### npx @coinfello/agent-cli create_account ``` -openclaw create_account +npx @coinfello/agent-cli create_account ``` | Parameter | Type | Required | Description | @@ -45,18 +45,18 @@ openclaw create_account Generates a new private key automatically and saves it along with the smart account address and chain to config. -### openclaw get_account +### npx @coinfello/agent-cli get_account ``` -openclaw get_account +npx @coinfello/agent-cli get_account ``` No parameters. Prints the stored smart account address from config. Exits with an error if no account has been created. -### openclaw sign_in +### npx @coinfello/agent-cli sign_in ``` -openclaw sign_in [--base-url ] +npx @coinfello/agent-cli sign_in [--base-url ] ``` | Parameter | Type | Required | Default | Description | @@ -67,35 +67,34 @@ The default resolves using the `COINFELLO_BASE_URL` environment variable (defaul Performs a Sign-In with Ethereum (SIWE) flow using the private key from config. Saves the `session_token` to config on success. The session token is automatically injected as a cookie for subsequent API calls. -### openclaw set_delegation +### npx @coinfello/agent-cli set_delegation ``` -openclaw set_delegation +npx @coinfello/agent-cli set_delegation ``` | Parameter | Type | Required | Description | | ------------ | -------- | -------- | --------------------------------------------------------------- | | `delegation` | `string` | Yes | JSON-encoded Delegation object from MetaMask Smart Accounts Kit | -### openclaw send_prompt +### npx @coinfello/agent-cli send_prompt ``` -openclaw send_prompt [--use-redelegation] +npx @coinfello/agent-cli send_prompt ``` -| Parameter | Type | Required | Default | Description | -| -------------------- | --------- | -------- | ------- | ----------------------------------------------------------- | -| `prompt` | `string` | Yes | — | Natural language prompt to send to CoinFello | -| `--use-redelegation` | `boolean` | No | `false` | Use stored parent delegation to create a redelegation chain | +| Parameter | Type | Required | Default | Description | +| --------- | -------- | -------- | ------- | -------------------------------------------- | +| `prompt` | `string` | Yes | — | Natural language prompt to send to CoinFello | -The server determines whether a delegation is needed and, if so, what scope and chain to use. The client creates and signs the subdelegation based on the server's `ask_for_delegation` client tool call response. +The server determines whether a delegation is needed and, if so, what scope and chain to use. The client creates and signs the subdelegation based on the server's `ask_for_delegation` client tool call response. Each subdelegation is created with a unique random salt to ensure delegation uniqueness. **ERC-6492 signature wrapping**: If the smart account has not yet been deployed on-chain, the CLI wraps the delegation signature using ERC-6492 (`serializeErc6492Signature`) with the account's factory address and factory data. This allows the delegation to be verified even before the account contract exists. -### openclaw get_transaction_status +### npx @coinfello/agent-cli get_transaction_status ``` -openclaw get_transaction_status +npx @coinfello/agent-cli get_transaction_status ``` | Parameter | Type | Required | Description | @@ -223,11 +222,10 @@ All `amount` fields are in the token's smallest unit (e.g. `5000000` for 5 USDC ## Error Messages -| Error | Cause | Fix | -| ------------------------------------------------------------------------------ | ----------------------------------- | -------------------------------------- | -| `Unknown chain ""` | Invalid chain name | Use a valid viem chain name | -| `No private key found in config. Run 'create_account' first.` | Missing private key in config | Run `openclaw create_account ` | -| `No smart account found. Run 'create_account' first.` | Missing smart account in config | Run `openclaw create_account ` | -| `No chain found in config. Run 'create_account' first.` | Missing chain in config | Run `openclaw create_account ` | -| `--use-redelegation requires a parent delegation. Run 'set_delegation' first.` | No stored delegation | Run `openclaw set_delegation ''` | -| `No delegation request received from the server.` | Server returned unexpected response | Check the full response JSON printed | +| Error | Cause | Fix | +| ------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------- | +| `Unknown chain ""` | Invalid chain name | Use a valid viem chain name | +| `No private key found in config. Run 'create_account' first.` | Missing private key in config | Run `npx @coinfello/agent-cli create_account ` | +| `No smart account found. Run 'create_account' first.` | Missing smart account in config | Run `npx @coinfello/agent-cli create_account ` | +| `No chain found in config. Run 'create_account' first.` | Missing chain in config | Run `npx @coinfello/agent-cli create_account ` | +| `No delegation request received from the server.` | Server returned unexpected response | Check the full response JSON printed | diff --git a/coinfello/scripts/setup-and-send.sh b/coinfello/scripts/setup-and-send.sh index 55b630d..1b3d097 100644 --- a/coinfello/scripts/setup-and-send.sh +++ b/coinfello/scripts/setup-and-send.sh @@ -13,15 +13,15 @@ CHAIN="${1:?Usage: $0 }" PROMPT="${2:?Missing prompt}" echo "==> Creating smart account on ${CHAIN}..." -openclaw create_account "$CHAIN" +npx @coinfello/agent-cli create_account "$CHAIN" echo "" echo "==> Signing in..." -openclaw sign_in +npx @coinfello/agent-cli sign_in echo "" echo "==> Sending prompt..." -OUTPUT=$(openclaw send_prompt "$PROMPT") +OUTPUT=$(npx @coinfello/agent-cli send_prompt "$PROMPT") echo "$OUTPUT" @@ -31,5 +31,5 @@ TXN_ID=$(echo "$OUTPUT" | grep -oP 'Transaction ID: \K.*') if [ -n "$TXN_ID" ]; then echo "" echo "==> Checking transaction status..." - openclaw get_transaction_status "$TXN_ID" + npx @coinfello/agent-cli get_transaction_status "$TXN_ID" fi diff --git a/package.json b/package.json index b94a838..68e7be7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coinfello/agent-cli", - "version": "0.1.0", + "version": "0.1.1", "description": "", "type": "module", "main": "dist/index.js", diff --git a/src/account.ts b/src/account.ts index 0752841..13f75ef 100644 --- a/src/account.ts +++ b/src/account.ts @@ -9,6 +9,7 @@ import { import { PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts' import { createPublicClient, http, type Hex, type Chain } from 'viem' import * as chains from 'viem/chains' +import { randomBytes } from 'node:crypto' export type HybridSmartAccount = ToMetaMaskSmartAccountReturnType export type DelegationScope = CreateDelegationOptions['scope'] @@ -90,5 +91,6 @@ export function createSubdelegation({ from: smartAccount.address, parentDelegation, environment: smartAccount.environment, + salt: `0x${randomBytes(32).toString('hex')}` as Hex, }) } diff --git a/src/index.ts b/src/index.ts index a8e5efb..87fd5ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -122,139 +122,124 @@ program .command('send_prompt') .description('Send a prompt to CoinFello, creating a delegation if requested by the server') .argument('', 'The prompt to send') - .option('--use-redelegation', 'Create a redelegation from a stored parent delegation') - .action( - async ( - prompt: string, - opts: { - useRedelegation?: boolean + .action(async (prompt: string) => { + try { + const config = await loadConfig() + if (!config.private_key) { + console.error("Error: No private key found in config. Run 'create_account' first.") + process.exit(1) + } + if (!config.smart_account_address) { + console.error("Error: No smart account found. Run 'create_account' first.") + process.exit(1) + } + if (!config.chain) { + console.error("Error: No chain found in config. Run 'create_account' first.") + process.exit(1) } - ) => { - try { - const config = await loadConfig() - if (!config.private_key) { - console.error("Error: No private key found in config. Run 'create_account' first.") - process.exit(1) - } - if (!config.smart_account_address) { - console.error("Error: No smart account found. Run 'create_account' first.") - process.exit(1) - } - if (!config.chain) { - console.error("Error: No chain found in config. Run 'create_account' first.") - process.exit(1) - } - if (opts.useRedelegation && !config.delegation) { - console.error( - "Error: --use-redelegation requires a parent delegation. Run 'set_delegation' first." - ) - process.exit(1) - } - // Load persisted session token into cookie jar - if (config.session_token) { - await loadSessionToken(config.session_token, BASE_URL_V1) - } + // Load persisted session token into cookie jar + if (config.session_token) { + await loadSessionToken(config.session_token, BASE_URL_V1) + } - // 1. Send prompt-only to conversation endpoint - console.log('Sending prompt...') - const initialResponse = await sendConversation({ - prompt, - }) + // 1. Send prompt-only to conversation endpoint + console.log('Sending prompt...') + const initialResponse = await sendConversation({ + prompt, + }) - // Read-only response: no tool calls and no transaction - if (!initialResponse.clientToolCalls?.length && !initialResponse.txn_id) { - console.log(initialResponse.responseText ?? '') - return - } + // Read-only response: no tool calls and no transaction + if (!initialResponse.clientToolCalls?.length && !initialResponse.txn_id) { + console.log(initialResponse.responseText ?? '') + return + } - // If we got a direct txn_id with no tool calls, we're done - if (initialResponse.txn_id && !initialResponse.clientToolCalls?.length) { - console.log('Transaction submitted successfully.') - console.log(`Transaction ID: ${initialResponse.txn_id}`) - return - } + // If we got a direct txn_id with no tool calls, we're done + if (initialResponse.txn_id && !initialResponse.clientToolCalls?.length) { + console.log('Transaction submitted successfully.') + console.log(`Transaction ID: ${initialResponse.txn_id}`) + return + } - // 2. Look for ask_for_delegation tool call - const delegationToolCall = initialResponse.clientToolCalls?.find( - (tc) => tc.name === 'ask_for_delegation' - ) - if (!delegationToolCall) { - console.error('Error: No delegation request received from the server.') - console.log('Response:', JSON.stringify(initialResponse, null, 2)) - process.exit(1) - } + // 2. Look for ask_for_delegation tool call + const delegationToolCall = initialResponse.clientToolCalls?.find( + (tc) => tc.name === 'ask_for_delegation' + ) + if (!delegationToolCall) { + console.error('Error: No delegation request received from the server.') + console.log('Response:', JSON.stringify(initialResponse, null, 2)) + process.exit(1) + } - // 3. Parse tool call arguments - /* eslint-disable-next-line */ - const args = JSON.parse(delegationToolCall.arguments) as any - console.log(`Delegation requested: scope=${args.scope.type}, chainId=${args.chainId}`) + // 3. Parse tool call arguments + /* eslint-disable-next-line */ + const args = JSON.parse(delegationToolCall.arguments) as any + console.log(`Delegation requested: scope=${args.scope.type}, chainId=${args.chainId}`) - // 4. Get CoinFello delegate address - console.log('Fetching CoinFello delegate address...') - const delegateAddress = await getCoinFelloAddress() + // 4. Get CoinFello delegate address + console.log('Fetching CoinFello delegate address...') + const delegateAddress = await getCoinFelloAddress() - // 5. Rebuild smart account using chainId from tool call - console.log('Loading smart account...') - const smartAccount = await getSmartAccount(config.private_key as Hex, args.chainId) + // 5. Rebuild smart account using chainId from tool call + console.log('Loading smart account...') + const smartAccount = await getSmartAccount(config.private_key as Hex, args.chainId) - // 6. Parse scope and create subdelegation - const scope = parseScope(args.scope) - console.log('Creating subdelegation...') - const subdelegation = createSubdelegation({ - smartAccount, - delegateAddress: delegateAddress as Hex, - parentDelegation: opts.useRedelegation ? config.delegation : undefined, - scope, - }) + // 6. Parse scope and create subdelegation + const scope = parseScope(args.scope) + console.log('Creating subdelegation...') + const subdelegation = createSubdelegation({ + smartAccount, + delegateAddress: delegateAddress as Hex, + scope, + }) - // 7. Sign the subdelegation - console.log('Signing subdelegation...') - const signature = await smartAccount.signDelegation({ - delegation: subdelegation, - }) - let sig = signature - const chain = resolveChainInput(config.chain) + // 7. Sign the subdelegation + console.log('Signing subdelegation...') + const signature = await smartAccount.signDelegation({ + delegation: subdelegation, + }) + let sig = signature + const chain = resolveChainInput(config.chain) - const publicClient = createPublicClient({ - chain, - transport: http(), + const publicClient = createPublicClient({ + chain, + transport: http(), + }) + const code = await publicClient.getCode({ address: smartAccount.address }) + const isDeployed = !!(code && code !== '0x') + if (!isDeployed) { + const factoryArgs = await smartAccount.getFactoryArgs() + sig = serializeErc6492Signature({ + signature, + address: factoryArgs.factory as `0x${string}`, + data: factoryArgs.factoryData as `0x${string}`, }) - const code = await publicClient.getCode({ address: smartAccount.address }) - const isDeployed = !!(code && code !== '0x') - if (!isDeployed) { - const factoryArgs = await smartAccount.getFactoryArgs() - sig = serializeErc6492Signature({ - signature, - address: factoryArgs.factory as `0x${string}`, - data: factoryArgs.factoryData as `0x${string}`, - }) - } + } - const signedSubdelegation: SignedSubdelegation = { ...subdelegation, signature: sig } + const signedSubdelegation: SignedSubdelegation = { ...subdelegation, signature: sig } - // 8. Send signed delegation back to conversation endpoint - console.log('Sending signed delegation...') - const finalResponse = await sendConversation({ - prompt: 'Please refer to the previous conversation messages and redeem this delegation.', - signedSubdelegation, - chatId: initialResponse.chatId, - delegationArguments: JSON.stringify(args), - callId: delegationToolCall.callId, - }) + // 8. Send signed delegation back to conversation endpoint + console.log('Sending signed delegation...') + const finalResponse = await sendConversation({ + prompt: 'Please refer to the previous conversation messages and redeem this delegation.', + signedSubdelegation, + chatId: initialResponse.chatId, + delegationArguments: JSON.stringify(args), + callId: delegationToolCall.callId, + }) - if (finalResponse.txn_id) { - console.log('Transaction submitted successfully.') - console.log(`Transaction ID: ${finalResponse.txn_id}`) - } else { - console.log('Final Response:', JSON.stringify(finalResponse, null, 2)) - } - } catch (err) { - console.error(`Failed to send prompt: ${(err as Error).message}`) - process.exit(1) + if (finalResponse.txn_id) { + console.log('Transaction submitted successfully.') + console.log(`Transaction ID: ${finalResponse.txn_id}`) + } else { + console.log('Final Response:', JSON.stringify(finalResponse, null, 2)) } + } catch (err) { + console.error(`Failed to send prompt: ${(err as Error).message}`) + process.exit(1) } - ) + }) // ── get_transaction_status ────────────────────────────────────── program diff --git a/tests/e2e/send-prompt-cli.test.ts b/tests/e2e/send-prompt-cli.test.ts index 01590f2..5256567 100644 --- a/tests/e2e/send-prompt-cli.test.ts +++ b/tests/e2e/send-prompt-cli.test.ts @@ -23,7 +23,7 @@ function runCli( ): Promise<{ stdout: string; stderr: string; exitCode: number }> { return new Promise((resolve) => { const child = spawn("node", [CLI_PATH, ...args], { - timeout: 180_000, + timeout: 360_000, }); let stdout = ""; let stderr = ""; @@ -79,7 +79,7 @@ describe("send_prompt CLI end-to-end", () => { it("completes the delegation flow when asked to send ETH via the CLI", async () => { const { stdout, stderr} = await runCli([ "send_prompt", - "send 0.001 ETH on Base Sepolia to 0x000000000000000000000000000000000000dEaD. call ask_for_delegation", + "send 0.0001 ETH on Base Sepolia to 0x000000000000000000000000000000000000dEaD. call ask_for_delegation", ]); console.log(stdout) @@ -90,5 +90,19 @@ describe("send_prompt CLI end-to-end", () => { expect(stdout).toContain("Creating subdelegation..."); expect(stdout).toContain("Signing subdelegation..."); expect(stdout).toContain("Sending signed delegation..."); + + const { stdout: stdout2, stderr: stderr2} = await runCli([ + "send_prompt", + "send 0.0001 ETH on Base Sepolia to 0x000000000000000000000000000000000000dEaD. call ask_for_delegation", + ]); + + console.log(stdout2) + console.error(stderr2) + + expect(stdout).toContain("Sending prompt..."); + expect(stdout).toContain("Delegation requested"); + expect(stdout).toContain("Creating subdelegation..."); + expect(stdout).toContain("Signing subdelegation..."); + expect(stdout).toContain("Sending signed delegation..."); }); }); diff --git a/vite.config.ts b/vite.config.ts index ae325e6..9855f1e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -26,6 +26,6 @@ export default defineConfig({ minify: false, }, test: { - testTimeout: 180_000, + testTimeout: 360_000, }, });