Skip to content

Commit a3f5541

Browse files
committed
feat(cli): simplified send and deposit commands
1 parent bf818f2 commit a3f5541

File tree

19 files changed

+814
-870
lines changed

19 files changed

+814
-870
lines changed

.changeset/all-carpets-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cartesi/cli": patch
3+
---
4+
5+
replaced `cartesi send erc20` by `cartesi deposit erc20`

.changeset/big-carrots-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cartesi/cli": patch
3+
---
4+
5+
replaced `cartesi send generic` by `cartesi send`

.changeset/dull-ways-sin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cartesi/cli": patch
3+
---
4+
5+
replaced `cartesi send erc721` by `cartesi deposit erc721`

.changeset/nice-knives-refuse.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cartesi/cli": patch
3+
---
4+
5+
replaced `cartesi send ether` by `cartesi deposit ether`

apps/cli/src/base.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ export const getProjectName = (options: { projectName?: string }) => {
5656

5757
export type AddressBook = Record<string, Address>;
5858

59-
export const getAddressBook = async (): Promise<AddressBook> => {
60-
const applicationAddress = await getApplicationAddress();
59+
export const getAddressBook = async (options: {
60+
projectName?: string;
61+
}): Promise<AddressBook> => {
62+
const applicationAddress = await getApplicationAddress(options);
6163

6264
// build rollups contracts address book
6365
const contracts: AddressBook = {

apps/cli/src/commands/address-book.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { Command } from "@commander-js/extra-typings";
22
import Table from "cli-table3";
3-
import { getAddressBook } from "../base.js";
3+
import { getAddressBook, getProjectName } from "../base.js";
44

55
export const createAddressBookCommand = () => {
66
return new Command("address-book")
77
.description(
88
"Prints the addresses of all smart contracts deployed to the runtime environment of the application.",
99
)
1010
.option("--json", "Format output as json.")
11-
.action(async ({ json }) => {
12-
const addressBook = await getAddressBook();
11+
.option(
12+
"--project-name <string>",
13+
"name of project (used by docker compose and cartesi-rollups-node)",
14+
)
15+
.action(async (options) => {
16+
const { json } = options;
17+
const projectName = getProjectName(options);
18+
const addressBook = await getAddressBook({ projectName });
1319
if (!json) {
1420
// print as a table
1521
const table = new Table({

apps/cli/src/commands/deposit.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Command } from "@commander-js/extra-typings";
2+
import select from "@inquirer/select";
3+
import { createErc20Command } from "./deposit/erc20.js";
4+
import { createErc721Command } from "./deposit/erc721.js";
5+
import { createEtherCommand } from "./deposit/ether.js";
6+
7+
export const createDepositCommand = () => {
8+
const command = new Command("deposit")
9+
.description("Deposits an asset to the application")
10+
.configureHelp({ showGlobalOptions: true })
11+
.option("--from <address>", "input sender address")
12+
.option("--application <address>", "application address")
13+
.option(
14+
"--project-name <string>",
15+
"name of project (used by docker compose and cartesi-rollups-node)",
16+
)
17+
.option("--rpc-url <url>", "RPC URL of the Cartesi Devnet")
18+
.action(async (options, command) => {
19+
// get registered subcommands
20+
const commands = command.commands;
21+
22+
// create choices for the selected prompt based on registered subcommands
23+
const choices = commands.map((cmd) => ({
24+
name: cmd.name(),
25+
value: cmd,
26+
description: cmd.description(),
27+
}));
28+
29+
const subcommand = await select({
30+
choices,
31+
message: "Select type of asset to deposit",
32+
});
33+
34+
// execute selected subcommand
35+
subcommand.parseAsync(command.args);
36+
});
37+
38+
command.addCommand(createEtherCommand());
39+
command.addCommand(createErc20Command());
40+
command.addCommand(createErc721Command());
41+
// command.addCommand(createErc1155BatchCommand());
42+
// command.addCommand(createErc1155SingleCommand());
43+
44+
return command;
45+
};
46+
47+
export type DepositCommandOpts = ReturnType<
48+
ReturnType<typeof createDepositCommand>["opts"]
49+
>;
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { Command } from "@commander-js/extra-typings";
2+
import chalk from "chalk";
3+
import ora from "ora";
4+
import {
5+
type Address,
6+
type PublicClient,
7+
erc20Abi,
8+
formatUnits,
9+
getAddress,
10+
isAddress,
11+
isHex,
12+
parseUnits,
13+
} from "viem";
14+
import { getProjectName } from "../../base.js";
15+
import {
16+
erc20PortalAbi,
17+
erc20PortalAddress,
18+
testTokenAddress,
19+
} from "../../contracts.js";
20+
import {
21+
addressInput,
22+
bigintInput,
23+
getInputApplicationAddress,
24+
} from "../../prompts.js";
25+
import { connect } from "../../wallet.js";
26+
import type { DepositCommandOpts } from "../deposit.js";
27+
28+
type ERC20Token = {
29+
address: Address;
30+
name: string;
31+
symbol: string;
32+
decimals: number;
33+
};
34+
35+
const readToken = async (
36+
publicClient: PublicClient,
37+
address: Address,
38+
): Promise<ERC20Token> => {
39+
const args = { abi: erc20Abi, address };
40+
const symbol = await publicClient.readContract({
41+
...args,
42+
functionName: "symbol",
43+
});
44+
const name = await publicClient.readContract({
45+
...args,
46+
functionName: "name",
47+
});
48+
const decimals = await publicClient.readContract({
49+
...args,
50+
functionName: "decimals",
51+
});
52+
return {
53+
address,
54+
name,
55+
symbol,
56+
decimals,
57+
};
58+
};
59+
60+
const parseToken = async (options: {
61+
testClient: PublicClient;
62+
token?: string;
63+
}): Promise<ERC20Token> => {
64+
const { testClient } = options;
65+
66+
const address =
67+
options.token && isAddress(options.token)
68+
? getAddress(options.token)
69+
: await addressInput({
70+
message: "Token address",
71+
default: testTokenAddress,
72+
});
73+
74+
return readToken(testClient, address);
75+
};
76+
77+
export const createErc20Command = () => {
78+
// biome-ignore lint/complexity/noBannedTypes: commander pattern
79+
return new Command<[], {}, DepositCommandOpts>("erc20")
80+
.description("Deposit ERC-20 to the application")
81+
.configureHelp({ showGlobalOptions: true })
82+
.option("--token <address>", "token address")
83+
.option("--amount <number>", "amount to send")
84+
.option("--exec-layer-data <hex>", "exec layer data", "0x")
85+
.action(async (options, command) => {
86+
const { from } = command.optsWithGlobals();
87+
88+
const projectName = getProjectName(command.optsWithGlobals());
89+
90+
// connect to anvil
91+
const testClient = await connect(command.optsWithGlobals());
92+
93+
// the input sender, impersonated
94+
const account =
95+
from && isAddress(from)
96+
? getAddress(from)
97+
: (await testClient.getAddresses())[0];
98+
99+
const token = await parseToken({
100+
testClient,
101+
token: options.token,
102+
});
103+
104+
// get dapp address from local node, or ask
105+
const application = await getInputApplicationAddress({
106+
...command.optsWithGlobals(),
107+
projectName,
108+
});
109+
110+
const { decimals, symbol } = token;
111+
const amount = options.amount
112+
? parseUnits(options.amount, decimals)
113+
: await bigintInput({
114+
message: `Amount (${symbol})`,
115+
decimals,
116+
});
117+
118+
const execLayerData = isHex(options.execLayerData)
119+
? options.execLayerData
120+
: "0x";
121+
122+
// progress spinner
123+
const progress = ora();
124+
125+
// check balance
126+
const balance = await testClient.readContract({
127+
abi: erc20Abi,
128+
address: token.address,
129+
functionName: "balanceOf",
130+
args: [account],
131+
});
132+
if (balance < amount) {
133+
progress.fail("Insufficient balance");
134+
return;
135+
}
136+
137+
// check allowance
138+
const allowance = await testClient.readContract({
139+
abi: erc20Abi,
140+
address: token.address,
141+
functionName: "allowance",
142+
args: [account, erc20PortalAddress],
143+
});
144+
145+
// for messages
146+
const amountStr = `${chalk.cyan(formatUnits(amount, decimals))} ${symbol}`;
147+
148+
// approve if needed
149+
if (allowance < amount) {
150+
progress.start(`Approving ${amountStr}...`);
151+
const { request } = await testClient.simulateContract({
152+
abi: erc20Abi,
153+
account,
154+
address: token.address,
155+
functionName: "approve",
156+
args: [erc20PortalAddress, amount],
157+
});
158+
const hash = await testClient.writeContract(request);
159+
await testClient.waitForTransactionReceipt({ hash });
160+
progress.succeed(`Approved ${amountStr}`);
161+
}
162+
163+
// simulate deposit call
164+
const { request } = await testClient.simulateContract({
165+
abi: erc20PortalAbi,
166+
account,
167+
address: erc20PortalAddress,
168+
functionName: "depositERC20Tokens",
169+
args: [token.address, application, amount, execLayerData],
170+
});
171+
172+
// send deposit
173+
progress.start(
174+
`Depositing ${amountStr} to ${chalk.cyan(application)}...`,
175+
);
176+
const hash = await testClient.writeContract(request);
177+
await testClient.waitForTransactionReceipt({ hash });
178+
progress.succeed(
179+
`Deposited ${amountStr} to ${chalk.cyan(application)}`,
180+
);
181+
});
182+
};

0 commit comments

Comments
 (0)