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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ jobs:
- name: Install Dependencies
run: pnpm install --frozen-lockfile

- name: Install chakra-ui types
run: pnpm run chakra-types
- name: Generate chakra-ui types and contract ABIs
run: pnpm codegen

- name: Code format check
run: pnpm format:check
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/chromatic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
- name: Install Dependencies
run: pnpm install --frozen-lockfile

- name: Generate chakra-ui types and contract ABIs
run: pnpm codegen

- name: Build Storybook
run: pnpm build-storybook

Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,9 @@ yarn-error.log*

# pnpm
.pnpm-store/

# typescript
tsconfig.tsbuildinfo

# generated by `pnpm gen:abi` (wagmi cli)
src/services/contracts/generated.ts
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ coverage
.vscode
# regenerated by next on every build, and marked "should not be edited"
next-env.d.ts
# generated by `pnpm gen:abi` (wagmi cli)
src/services/contracts/generated.ts
12 changes: 11 additions & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ import { FC } from 'react';
import { Fonts } from '../src/components/Fonts';
import { SelectedChain } from '../src/components/header';
import { ColorModeProvider } from '../src/components/ui/color-mode';
import { mock } from 'wagmi/connectors';
import Web3Container from '../src/components/Web3Container';
import { createWagmiConfig } from '../src/components/wallet/config';
import theme from '../src/styles/theme';
import ColorMode from './ColorMode';

// A mock connector keeps Storybook and Chromatic from instantiating WalletConnect.
const storybookWagmiConfig = createWagmiConfig([
mock({
accounts: ['0x491604c0FDF08347Dd1fa4Ee062a822A5DD06B5D'],
features: { reconnect: false },
}),
]);

const withChakra = (Story: FC, context: StoryContext) => {
console.info(`withChakra`);
const colorModeValue = context.globals.backgrounds?.value ?? 'dark';
Expand All @@ -21,7 +31,7 @@ const withChakra = (Story: FC, context: StoryContext) => {
<Fonts />
<ColorModeProvider>
<ColorMode value={colorModeValue} />
<Web3Container>
<Web3Container config={storybookWagmiConfig}>
<SelectedChain />
<Story />
</Web3Container>
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Replace the unmaintained web3-onboard wallet library with RainbowKit on wagmi 2 and viem.
- Connect through MetaMask, WalletConnect (which also reaches Ledger Live), Base (Coinbase Wallet) and Safe, which connects automatically when the explorer is opened as a Safe App.
- Move the contract and services layer from ethers v5 to viem, with ABIs and deployment addresses generated by `@wagmi/cli` (`pnpm codegen`) from the `@cartesi` contract packages.
- Resolve ENS avatars on the server through viem's universal resolver.
- Move `ethers` and the `@cartesi` contract packages to devDependencies (ABI generation only) and remove `@ethersproject/*`, `eth-chains` and `eth-rpc-errors`; the `elliptic` advisory no longer reaches the production dependency tree.

## [3.12.8] - 2026-09-23

- Upgrade dependencies to keep maintenance with latest releases.
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,42 @@ The following supply-chain protections are enabled:
`overrides` is used to pin transitive dependencies away from vulnerable or
non-registry sources.

## Wallet connection and contracts

The wallet layer lives in `src/components/wallet/` and is the only place that
imports [RainbowKit](https://rainbowkit.com) and [wagmi](https://wagmi.sh); the
rest of the application consumes the connection through `useWallet()`, which
exposes the account, the chain id and the viem `publicClient`/`walletClient`.
Swapping the wallet library again means changing that folder only.

- `config.ts` holds the supported chains, the RPC transports and the wallet list
shown by the RainbowKit modal: MetaMask, WalletConnect (also the way to reach
Ledger Live), Safe and Base (Coinbase Wallet), plus any browser wallet it
detects through EIP-6963. RainbowKit
renders its own wallet list on mobile, so breadth comes from that list.
- Safe only appears when the explorer is loaded as a Safe App (see
`public/manifest.json`) and connects automatically on load.
- `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` (WalletConnect Cloud / Reown dashboard)
and `NEXT_PUBLIC_DAPP_URL` are required; the RPC endpoints come from
`NEXT_PUBLIC_RPC_URL_<chainId>`.
- RainbowKit 2 pins wagmi to the 2.x line, which froze when wagmi 3 shipped. The
bundled WalletConnect and MetaMask SDK versions move only when RainbowKit adds
wagmi 3 support; this is a known, accepted trade-off.

Contracts are called through [viem](https://viem.sh) using ABIs and deployment
addresses generated into `src/services/contracts/generated.ts` (gitignored) by
`pnpm gen:abi`, part of `pnpm codegen` together with the Chakra UI type
generation. `pnpm dev`, `pnpm build` and CI run `codegen` first; run it once
after `pnpm install` before `pnpm test` or `pnpm storybook`. `wagmi.config.ts`
lists every contract and the `@cartesi` package it comes from. Those packages
and `ethers` are devDependencies used only by the generator: an ESLint rule
rejects importing them from `src/`.

Addresses coming from the subgraph are lowercase, while contract reads return
checksummed addresses. Keep comparing `account` (already lowercase) with
subgraph values as the code does today, and compare contract results with
viem's `isAddressEqual` rather than `===`.

## Running locally

This is a [Next.js](https://nextjs.org) application which uses smart contracts deployed by the [pos-dlib](https://github.com/cartesi/pos-dlib) and [staking-pool](https://github.com/cartesi/staking-pool) projects.
Expand Down Expand Up @@ -136,6 +172,9 @@ Simply run:
$ pnpm dev
```

This runs the database migration and `pnpm codegen` (Chakra UI types and the
contract ABIs) before starting Next.js.

### Open application and setup MetaMask

Open the application in your browser at http://localhost:3000
Expand Down
16 changes: 6 additions & 10 deletions __tests__/components/Layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU General Public License for more details.

import { PoS, StakingImpl } from '@cartesi/pos';
import { StakingPoolFactoryImpl } from '@cartesi/staking-pool';
import { CartesiToken, SimpleFaucet } from '@cartesi/token';
import { WorkerManagerAuthManagerImpl } from '@cartesi/util';
import { render, screen } from '@testing-library/react';
import Layout, { footerLinks, headerLinks } from '../../src/components/Layout';
import useMeta from '../../src/graphql/hooks/useMeta';
Expand Down Expand Up @@ -80,22 +76,22 @@ describe('Layout component', () => {
beforeEach(() => {
mockedUsePoSContract.mockReturnValue({
address,
} as unknown as PoS);
} as unknown as ReturnType<typeof usePoSContract>);
mockedUseCartesiTokenContract.mockReturnValue({
address,
} as unknown as CartesiToken);
} as unknown as ReturnType<typeof useCartesiTokenContract>);
mockedUseSimpleFaucetContract.mockReturnValue({
address,
} as unknown as SimpleFaucet);
} as unknown as ReturnType<typeof useSimpleFaucetContract>);
mockedUseStakingContract.mockReturnValue({
address,
} as unknown as StakingImpl);
} as unknown as ReturnType<typeof useStakingContract>);
mockedUseStakingPoolFactoryContract.mockReturnValue({
address,
} as unknown as StakingPoolFactoryImpl);
} as unknown as ReturnType<typeof useStakingPoolFactoryContract>);
mockedUseWorkerManagerContract.mockReturnValue({
address,
} as unknown as WorkerManagerAuthManagerImpl);
} as unknown as ReturnType<typeof useWorkerManagerContract>);
mockedUseMeta.mockReturnValue(undefined);
});

Expand Down
10 changes: 7 additions & 3 deletions __tests__/components/TransactionFeedback.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@

import { render, screen, fireEvent } from '@testing-library/react';
import TransactionFeedback from '../../src/components/TransactionFeedback';
import { Transaction } from '../../src/services/transaction';
import {
SubmittedTransaction,
Transaction,
} from '../../src/services/transaction';
import { withChakraTheme } from '../test-utilities';
import { ContractTransaction } from 'ethers';

const Component = withChakraTheme(TransactionFeedback);
const props = {
transaction: {
submitting: false,
acknowledged: false,
confirmations: 0,
set: jest.fn(),
ack: jest.fn(),
} as Transaction<string>,
Expand Down Expand Up @@ -65,7 +68,8 @@ describe('TransactionFeedback component', () => {
...props.transaction,
transaction: {
hash: '0x6200d8606aab695a7f730a3f7c60e399eb3bd10f',
} as ContractTransaction,
chainId: 1,
} satisfies SubmittedTransaction,
};
render(<Component transaction={{ ...transaction }} />);

Expand Down
3 changes: 1 addition & 2 deletions __tests__/components/node/NodeAllowanceSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
// PARTICULAR PURPOSE. See the GNU General Public License for more details.

import { fireEvent, render } from '@testing-library/react';
import { BigNumber } from 'ethers';
import { NodeAllowanceSection } from '../../../src/components/node/NodeAllowanceSection';
import { withChakraTheme } from '../../test-utilities';

Expand All @@ -21,7 +20,7 @@ describe('NodeAllowanceSection component', () => {
const mockedOnAllowanceClick = jest.fn();
const { container } = render(
<Component
allowance={BigNumber.from('10000')}
allowance={10000n}
onAllowanceClick={mockedOnAllowanceClick}
/>,
);
Expand Down
5 changes: 2 additions & 3 deletions __tests__/components/node/NodeInfoSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
// PARTICULAR PURPOSE. See the GNU General Public License for more details.
import { useMediaQuery } from '@chakra-ui/react';
import { render, screen } from '@testing-library/react';
import { BigNumber } from 'ethers';
import { NodeInfoSection } from '../../../src/components/node/NodeInfoSection';
import { useUserNodes } from '../../../src/graphql/hooks/useNodes';
import { truncateString } from '../../../src/utils/stringUtils';
Expand All @@ -23,8 +22,8 @@ const NODE_BALANCE_ETH = '0.1868';
const defaultProps = {
account: '0xabe5271e041df23c9f7c0461df5d340a0c1c36f4',
address: '0xb00299b573a9deee20e6a242416188d1033e325f',
userBalance: BigNumber.from('0x029799b68c5fbbd4'),
nodeBalance: BigNumber.from('0x029799b68c5fbbd4'),
userBalance: BigInt('0x029799b68c5fbbd4'),
nodeBalance: BigInt('0x029799b68c5fbbd4'),
onRetire: null,
onHire: null,
onDeposit: null,
Expand Down
5 changes: 2 additions & 3 deletions __tests__/components/node/NodeMaturingSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,18 @@
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU General Public License for more details.
import { render, screen } from '@testing-library/react';
import { BigNumber } from 'ethers';
import { NodeMaturingSection } from '../../../src/components/node/NodeMaturingSection';
import { withChakraTheme } from '../../test-utilities';

const Component = withChakraTheme(NodeMaturingSection);

const TEST_MATURING_BALANCE = BigNumber.from('0x04b75e170de2fc0000');
const TEST_MATURING_BALANCE = BigInt('0x04b75e170de2fc0000');
const TEST_MATURING_BALANCE_CTSI = '87';
const TEST_MATURING_TIME = '1 hour, 20 minutes';

describe('NodeMaturingSection component', () => {
it('Should be with 0 CTSI, 6 hours', () => {
render(<Component maturingBalance={BigNumber.from(0)} />);
render(<Component maturingBalance={0n} />);

expect(
screen.getByText(
Expand Down
5 changes: 2 additions & 3 deletions __tests__/components/node/NodeReleasingSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU General Public License for more details.
import { render, screen } from '@testing-library/react';
import { BigNumber } from 'ethers';
import { NodeReleasingSection } from '../../../src/components/node/NodeReleasingSection';
import { withChakraTheme } from '../../test-utilities';

const Component = withChakraTheme(NodeReleasingSection);

const TEST_RELEASING_BALANCE = BigNumber.from('0x04b75e170de2fc0000');
const TEST_RELEASING_BALANCE = BigInt('0x04b75e170de2fc0000');
const TEST_RELEASING_BALANCE_CTSI = '87';
const TEST_RELEASING_TIME = '1 hour, 20 minutes';
const TEST_WITHDRAW_BUTTON = `WITHDRAW (${TEST_RELEASING_TIME})`;
Expand Down Expand Up @@ -66,7 +65,7 @@ describe('NodeReleasingSection component', () => {
it(`Should be 'Released' with 0 CTSI, no Withdraw button`, () => {
render(
<Component
releasingBalance={BigNumber.from('0')}
releasingBalance={0n}
releasingLeftShort={null}
onWithdraw={null}
/>,
Expand Down
3 changes: 1 addition & 2 deletions __tests__/components/node/NodeStakedBalanceSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU General Public License for more details.
import { render, screen } from '@testing-library/react';
import { BigNumber } from 'ethers';
import { NodeStakedBalanceSection } from '../../../src/components/node/NodeStakedBalanceSection';
import { withChakraTheme } from '../../test-utilities';

const Component = withChakraTheme(NodeStakedBalanceSection);

const TEST_BALANCE = BigNumber.from('0x04b75e170de2fc0000');
const TEST_BALANCE = BigInt('0x04b75e170de2fc0000');
const TEST_BALANCE_CTSI = '87';

describe('NodeStakedBalanceSection component', () => {
Expand Down
28 changes: 13 additions & 15 deletions __tests__/components/node/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,34 @@
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU General Public License for more details.

import { ContractReceipt } from 'ethers/lib';
import { mock } from 'jest-mock-extended';
import { cloneDeep, cond, constant, isEqual, stubTrue } from 'lodash/fp';
import { TransactionReceipt } from 'viem';
import { Node, NodeStatus } from '../../../src/services/node';
import { useStaking } from '../../../src/services/staking';
import { useCartesiToken } from '../../../src/services/token';
import { Transaction } from '../../../src/services/transaction';
import { toBigNumber } from '../../../src/utils/numberParser';
import { ReturnOf } from '../../test-utilities';

/**
* Don't use this directly. Use the build function exported below
* (cast because viem's Log type only resolves `topics` with strictNullChecks)
*/
const stubReceipt: ContractReceipt = {
const stubReceipt = {
blockHash:
'0x8a179bc6cb299f936c4fd614995e62d597ec6108b579c23034fb220967ceaa94',
blockNumber: 12598244,
byzantium: true,
confirmations: 2083037,
blockNumber: 12598244n,
contractAddress: '0x733aF852514e910E2f8af40d61E00530377889E9',
cumulativeGasUsed: toBigNumber('12102324'),
effectiveGasPrice: toBigNumber('10100000000'),
cumulativeGasUsed: 12102324n,
effectiveGasPrice: 10100000000n,
from: '0x8ba1f109551bD432803012645Ac136ddd64DBA72',
gasUsed: toBigNumber('443560'),
gasUsed: 443560n,
logs: [
{
address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
blockHash:
'0x8a179bc6cb299f936c4fd614995e62d597ec6108b579c23034fb220967ceaa94',
blockNumber: 12598244,
blockNumber: 12598244n,
data: '0x000000000000000000000000084b1c3c81545d370f3634392de611caabff8148',
logIndex: 160,
topics: [
Expand All @@ -56,7 +54,7 @@ const stubReceipt: ContractReceipt = {
address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
blockHash:
'0x8a179bc6cb299f936c4fd614995e62d597ec6108b579c23034fb220967ceaa94',
blockNumber: 12598244,
blockNumber: 12598244n,
data: '0x000000000000000000000000a2c122be93b0074270ebee7f6b7292c7deb45047',
logIndex: 161,
topics: [
Expand All @@ -69,16 +67,15 @@ const stubReceipt: ContractReceipt = {
removed: false,
},
],
events: [],
logsBloom:
'0x00000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000004000000000000010000000000000020000000000000000000040000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000040000000000000000100004000000000000008000040000000000000000000000000000000000005000000000041000000000000000000000000000000000000000000000000100000000000000001000000000000000000000',
status: 1,
status: 'success',
to: null,
transactionHash:
'0x5b73e239c55d790e3c9c3bbb84092652db01bb8dbf49ccc9e4a318470419d9a0',
transactionIndex: 315,
type: 0,
};
type: 'legacy',
} as unknown as TransactionReceipt;

const initialNodeState: Node = {
address: undefined,
Expand Down Expand Up @@ -144,6 +141,7 @@ function buildTransaction(): Transaction<any> {
result: undefined,
set: jest.fn(),
submitting: false,
confirmations: 0,
transaction: undefined,
isOngoing: false,
state: 'acknowledged',
Expand Down
Loading
Loading