Skip to content
Merged
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ nova send <amount> [destination]
- `amount` — Amount to send
- `destination` *(optional)* — Recipient email or Nova account address

**Options**

- `-d, --dry-run` — Preview the transaction without submitting it

**Behavior**

- If `destination` **is provided**, funds are sent directly to that
Expand Down Expand Up @@ -291,6 +295,10 @@ nova withdraw <amount> <stablecoin> <address> <blockchain>
- `blockchain` — Target blockchain (required if it cannot be inferred
from the address)

**Options**

- `-d, --dry-run` — Preview the transaction without submitting it

#### `config`

Manage Nova configuration values.
Expand Down
53 changes: 38 additions & 15 deletions commands/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,32 +106,55 @@ program
.description("Send balance to another account")
.option("-j, --json", "Output results as JSON")
.option("-t, --toon", "Output results as TOON")
.option(
"-d, --dry-run",
"Validate and preview the transaction without submitting it",
)
.argument("amount", "The amount of balance to send", parseAmount)
.argument(
"[destination]",
"The email address or Mynth account address to send balance to. If omitted then a claim link will be created.",
parseDestination,
)
.action(async (amount: Decimal, destination?: string) => {
const privateKey = getPrivateKey();
if (privateKey) {
const sent = await sendWithPrivateKey(privateKey, amount, destination);
.action(
async (
amount: Decimal,
destination: string | undefined,
options: { dryRun?: boolean },
) => {
if (options.dryRun) {
const result: { dryRun: true; amount: string; to?: string } = {
dryRun: true,
amount: amount.toString(),
};
if (destination) result.to = destination;
printOk(
result,
`Would send ${amount}${destination ? ` to ${destination}` : ""}`,
);
return;
}

const privateKey = getPrivateKey();
if (privateKey) {
const sent = await sendWithPrivateKey(privateKey, amount, destination);
if (!sent.ok) return logExit(sent.error);

printOk(
createResult(amount, destination, sent),
`Sent ${amount} to ${destination ?? sent.data}; ${sent.data.txId}`,
);
Comment thread
SynthLuvr marked this conversation as resolved.
return;
}

const sent = await send(amount, destination, getNetwork());
if (!sent.ok) return logExit(sent.error);

printOk(
createResult(amount, destination, sent),
`Sent ${amount} to ${destination ?? sent.data}; ${sent.data.txId}`,
);
return;
}

const sent = await send(amount, destination, getNetwork());
if (!sent.ok) return logExit(sent.error);

printOk(
createResult(amount, destination, sent),
`Sent ${amount} to ${destination ?? sent.data}; ${sent.data.txId}`,
);
});
},
);

export { sendWithTokenOrKey };
23 changes: 23 additions & 0 deletions commands/withdraw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ program
.description("Withdraws balance to external blockchain")
.option("-j, --json", "Output results as JSON")
.option("-t, --toon", "Output results as TOON")
.option(
"-d, --dry-run",
"Validate and preview the transaction without submitting it",
)
.argument("amount", "The amount of balance to withdraw", parseAmount)
.argument(
"stablecoin",
Expand All @@ -58,7 +62,26 @@ program
stablecoin: string,
address: string,
blockchain: string,
options: { dryRun?: boolean },
) => {
if (options.dryRun) {
const token = resolveStablecoin(stablecoin, blockchain, getNetwork());
if (!token)
return logExit(`${stablecoin} does not exist for ${blockchain}`);

printOk(
{
dryRun: true,
amount: amount.toString(),
blockchain,
stablecoin,
to: address,
},
`Would withdraw ${amount} to ${address}`,
);
return;
}

const withdrawn = await withdraw(
amount,
stablecoin,
Expand Down
11 changes: 10 additions & 1 deletion skills/nova-wallet/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ Notes:
nova send <amount> [destination]
```

Options:

- `-d`, `--dry-run` — Validate and preview without submitting

Behavior:

- If `destination` omitted:
Expand All @@ -168,8 +172,9 @@ Behavior:
Rules:

- No interactive confirmation.
- No dry-run.
- Non-idempotent: re-running sends again.
- Use `--dry-run` (`-d`) to validate inputs and preview without
submitting.
- Always confirm with user whether `destination` is an email address or
wallet address before execution.

Expand Down Expand Up @@ -205,6 +210,10 @@ Properties (when `nova send <amount>` has no destination):
nova withdraw <amount> <stablecoin> <address> <blockchain>
```

Options:

- `-d`, `--dry-run` — Validate and preview without submitting

Rules:

- Confirm:
Expand Down
55 changes: 47 additions & 8 deletions skills/nova-wallet/references/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,16 @@ Wallet-address sends:
and does **not** prompt for confirmation.

- There is **no interactive confirmation step**
- There is **no dry-run mode**
- Sends are **not idempotent** (re-running the same command will send
funds again)
- Use `--dry-run` (`-d`) to validate inputs and preview the transaction
**without** submitting it

Options:

- `-j, --json` Output results as JSON
- `-t, --toon` Output results as TOON
- `-d, --dry-run` Validate and preview without submitting

Agents must:

Expand Down Expand Up @@ -327,6 +334,18 @@ Send to wallet address:
nova send 10 pcr6cdcvwjf9297vv6jmy8284xwlscspj2g0fw
```

Dry-run (preview without submitting):

``` bash
nova -j send --dry-run 10 friend@email.com
```

Example dry-run output (JSON):

``` bash
{"result":{"amount":"10","dryRun":true,"to":"friend@email.com"},"status":"ok"}
```

Best Practices for Agents:

- Confirm amount before sending
Expand All @@ -342,12 +361,30 @@ Best Practices for Agents:
nova withdraw <amount> <stablecoin> <address> <blockchain>
```

Options:

- `-j, --json` Output results as JSON
- `-t, --toon` Output results as TOON
- `-d, --dry-run` Validate and preview without submitting

Example:

``` bash
nova withdraw 10 USDC 0x7600eFB256ae7519e73C14a55152B0806b5cfF28 base
```

Dry-run (preview without submitting):

``` bash
nova -j withdraw --dry-run 10 USDC 0x7600eFB256ae7519e73C14a55152B0806b5cfF28 base
```

Example dry-run output (JSON):

``` bash
{"result":{"amount":"10","blockchain":"base","dryRun":true,"stablecoin":"USDC","to":"0x7600eFB256ae7519e73C14a55152B0806b5cfF28"},"status":"ok"}
```

Parameters:

- `amount` — numeric string
Expand Down Expand Up @@ -691,13 +728,15 @@ Never:

If user intent includes:

| Intent | Suggest |
|------------------|-----------------------------------|
| “receive funds” | `nova address` |
| “backup wallet” | `nova export phrase` |
| “send money” | `nova send` |
| “cash out” | `nova withdraw` |
| “switch testnet” | `nova config set network testnet` |
| Intent | Suggest |
|--------------------|-----------------------------------|
| “receive funds” | `nova address` |
| “backup wallet” | `nova export phrase` |
| “send money” | `nova send` |
| “preview send” | `nova send --dry-run` |
| “cash out” | `nova withdraw` |
| “preview withdraw” | `nova withdraw --dry-run` |
| “switch testnet” | `nova config set network testnet` |

## Summary

Expand Down
42 changes: 42 additions & 0 deletions tests/send.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect } from "vitest";
import { it } from "./base.js";

describe("nova send --dry-run", () => {
it("prints dry run result with destination", async ({ nova }) => {
const address = await nova(["address"]);
const stdout = await nova(["send", "--dry-run", "10", address]);
expect(stdout).toBe(`Would send 10 to ${address}`);
});

it("prints dry run result without destination", async ({ nova }) => {
const stdout = await nova(["send", "--dry-run", "5"]);
expect(stdout).toBe("Would send 5");
});

it("prints dry run result in JSON format", async ({ nova }) => {
const address = await nova(["address"]);
const stdout = await nova(["-j", "send", "--dry-run", "10", address]);
const result = JSON.parse(stdout);
expect(result.status).toBe("ok");
expect(result.result.dryRun).toBe(true);
expect(result.result.amount).toBe("10");
expect(result.result.to).toBe(address);
});

it("prints dry run result in JSON format without destination", async ({
nova,
}) => {
const stdout = await nova(["-j", "send", "--dry-run", "5"]);
const result = JSON.parse(stdout);
expect(result.status).toBe("ok");
expect(result.result.dryRun).toBe(true);
expect(result.result.amount).toBe("5");
expect(result.result.to).toBeUndefined();
});

it("accepts short flag -d", async ({ nova }) => {
const address = await nova(["address"]);
const stdout = await nova(["send", "-d", "10", address]);
expect(stdout).toBe(`Would send 10 to ${address}`);
});
});
65 changes: 65 additions & 0 deletions tests/withdraw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect } from "vitest";
import { it } from "./base.js";

const SOLANA_ADDRESS = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

describe("nova withdraw --dry-run", () => {
it("prints dry run result", async ({ nova }) => {
const stdout = await nova([
"withdraw",
"--dry-run",
"10",
"USDC",
SOLANA_ADDRESS,
"solana",
]);
expect(stdout).toBe(`Would withdraw 10 to ${SOLANA_ADDRESS}`);
});

it("prints dry run result in JSON format", async ({ nova }) => {
const stdout = await nova([
"-j",
"withdraw",
"--dry-run",
"10",
"USDC",
SOLANA_ADDRESS,
"solana",
]);
const result = JSON.parse(stdout);
expect(result.status).toBe("ok");
expect(result.result.dryRun).toBe(true);
expect(result.result.amount).toBe("10");
expect(result.result.stablecoin).toBe("USDC");
expect(result.result.blockchain).toBe("solana");
expect(result.result.to).toBe(SOLANA_ADDRESS);
});

it("errors when stablecoin does not exist for blockchain", async ({
nova,
}) => {
await expect(
nova([
"-j",
"withdraw",
"--dry-run",
"10",
"USDA",
SOLANA_ADDRESS,
"solana",
]),
).rejects.toThrow();
});

it("accepts short flag -d", async ({ nova }) => {
const stdout = await nova([
"withdraw",
"-d",
"10",
"USDC",
SOLANA_ADDRESS,
"solana",
]);
expect(stdout).toBe(`Would withdraw 10 to ${SOLANA_ADDRESS}`);
});
});