From 0d703446fc4d3a3421fa75d3f6c3b6ae768dc4d2 Mon Sep 17 00:00:00 2001 From: Ryan Berckmans Date: Fri, 12 Jul 2024 17:44:33 -0500 Subject: [PATCH] Move verifier into @3cities/verifier The package @3cities/verifier was added to contain the standalone static verifier. The package @3cities/core was added to contain dependencies shared across the verifier and interface. Many facilities were moved into @3cities/core. The root package.json tasks were standardized across subpackages using the new runTaskInAllPackages helper. The interface's tsconfig was updated to use module:"es2020" (old: "commonjs") and moduleResolution:"bundler" (old: "node"). This adds compatibility to import sub-exports like @3cities/core/proto/checkout-settings. However, a weird item is that type ConnectKitOptions could no longer be imported from any path. Dependencies shared between any two packages were lifted into the root package.json. Right now, that's @bufbuild/protobuf, @tanstack/react-query, immer, viem, and wagmi. --- .eslintrc.js | 4 -- package.json | 16 +++-- packages/core/.eslintrc.js | 11 +++ packages/core/.gitignore | 2 + packages/core/README.md | 6 ++ packages/{react-app => core}/buf.gen.yaml | 0 packages/{react-app => core}/buf.yaml | 0 packages/core/package.json | 55 +++++++++++++++ .../threecities/v1/checkout_settings.proto} | 12 +--- .../proto/threecities/v1/logical_assets.proto | 14 ++++ .../{react-app => core}/src/ExchangeRates.ts | 4 +- packages/{react-app => core}/src/IntRange.ts | 0 .../{react-app => core}/src/NonEmptyArray.ts | 0 packages/{react-app => core}/src/Optional.ts | 0 packages/{react-app => core}/src/Token.ts | 2 +- packages/{react-app => core}/src/Writable.ts | 0 packages/core/src/caip222StyleSignature.ts | 41 +++++++++++ packages/{react-app => core}/src/chains.ts | 2 +- .../threecities/v1/checkout_settings_pb.ts} | 46 +------------ .../gen/threecities/v1/logical_assets_pb.ts | 50 ++++++++++++++ .../src/getConfirmationsToWait.ts | 0 .../{react-app => core}/src/hasOwnProperty.ts | 0 packages/core/src/index.ts | 19 +++++ packages/core/src/isProduction.ts | 5 ++ .../{react-app => core}/src/logicalAssets.ts | 4 +- .../src/logicalAssetsToTokens.ts | 6 +- packages/{react-app => core}/src/rpcUrls.ts | 0 .../{react-app => core}/src/toUppercase.ts | 0 packages/{react-app => core}/src/tokens.ts | 7 +- packages/core/tsconfig.json | 6 ++ packages/eth-transfer-proxy/package.json | 4 +- packages/react-app/package.json | 14 ++-- packages/react-app/src/About.tsx | 5 +- .../react-app/src/ActiveDemoAccountContext.ts | 2 +- packages/react-app/src/AddressContext.ts | 4 +- packages/react-app/src/BuildInfo.tsx | 2 +- packages/react-app/src/CheckoutSettings.ts | 6 +- .../react-app/src/CheckoutSettingsContext.ts | 4 +- .../src/CheckoutSettingsProvider.tsx | 6 +- .../ConnectedAccountContextObserverContext.ts | 4 +- ...onnectedAccountContextObserverProvider.tsx | 11 +-- .../react-app/src/CurrencyAmountInput.tsx | 3 +- .../react-app/src/DemoAccountProvider.tsx | 4 +- .../react-app/src/ExchangeRatesContext.ts | 4 +- .../react-app/src/ExchangeRatesProvider.tsx | 8 +-- .../react-app/src/GlobalErrorBoundary.tsx | 2 +- packages/react-app/src/GlobalProviders.tsx | 3 +- .../IsPageVisibleOrRecentlyVisibleContext.ts | 2 +- ...IsPageVisibleOrRecentlyVisibleProvider.tsx | 4 +- packages/react-app/src/MainWrapper.tsx | 2 +- packages/react-app/src/MaybeUserPage.tsx | 8 +-- packages/react-app/src/Pay.tsx | 23 +++---- packages/react-app/src/Payment.ts | 10 +-- packages/react-app/src/QRCode.tsx | 2 +- .../react-app/src/RenderLiveTokenBalance.tsx | 2 +- .../src/RenderLogicalAssetAmount.tsx | 4 +- .../react-app/src/RenderRawTokenBalance.tsx | 8 +-- packages/react-app/src/RenderTokenBalance.tsx | 6 +- .../react-app/src/RenderTokenTransfer.tsx | 9 ++- packages/react-app/src/RequestMoney.tsx | 18 ++--- packages/react-app/src/TokenBalance.ts | 3 +- packages/react-app/src/blockExplorerUrls.ts | 2 +- packages/react-app/src/canAfford.ts | 4 +- packages/react-app/src/crypto.ts | 2 +- packages/react-app/{ => src}/images.d.ts | 0 packages/react-app/src/isProduction.ts | 5 -- packages/react-app/src/serialize.ts | 14 ++-- packages/react-app/src/strategies.ts | 20 ++---- packages/react-app/src/tokenTransfer.ts | 8 +-- packages/react-app/src/transactions.tsx | 18 +++-- .../react-app/src/useActiveDemoAccount.ts | 2 +- packages/react-app/src/useAsyncMemo.ts | 2 +- packages/react-app/src/useBestStrategy.ts | 4 +- .../react-app/src/useCaip222StyleSignature.ts | 69 ++++--------------- packages/react-app/src/useCheckoutSettings.ts | 8 +-- .../src/useConnectedAccountContext.ts | 4 +- packages/react-app/src/useEffectSkipFirst.ts | 2 +- packages/react-app/src/useEnsAddress.ts | 3 +- packages/react-app/src/useEnsName.ts | 3 +- packages/react-app/src/useExchangeRates.ts | 4 +- packages/react-app/src/useInput.tsx | 2 +- .../src/useIsPageVisibleOrRecentlyVisible.tsx | 2 +- .../useLogicalAssetTickerSelectionInput.tsx | 2 +- .../react-app/src/usePayWhatYouWantInput.tsx | 4 +- ...roposedPaymentReceiverAddressAndEnsName.ts | 2 +- packages/react-app/src/wagmiClient.ts | 3 +- packages/react-app/tsconfig.json | 4 +- packages/verifier/.eslintrc.js | 11 +++ packages/verifier/.gitignore | 2 + packages/verifier/README.md | 6 ++ packages/verifier/package.json | 45 ++++++++++++ packages/verifier/src/index.ts | 3 + .../src/verifyTransfer.ts} | 40 +++++------ packages/verifier/tsconfig.json | 6 ++ tsconfig.json | 2 +- yarn.lock | 47 ++++++++++++- 96 files changed, 522 insertions(+), 332 deletions(-) create mode 100644 packages/core/.eslintrc.js create mode 100644 packages/core/.gitignore create mode 100644 packages/core/README.md rename packages/{react-app => core}/buf.gen.yaml (100%) rename packages/{react-app => core}/buf.yaml (100%) create mode 100644 packages/core/package.json rename packages/{react-app/proto/threecities/v1/v1.proto => core/proto/threecities/v1/checkout_settings.proto} (97%) create mode 100644 packages/core/proto/threecities/v1/logical_assets.proto rename packages/{react-app => core}/src/ExchangeRates.ts (98%) rename packages/{react-app => core}/src/IntRange.ts (100%) rename packages/{react-app => core}/src/NonEmptyArray.ts (100%) rename packages/{react-app => core}/src/Optional.ts (100%) rename packages/{react-app => core}/src/Token.ts (98%) rename packages/{react-app => core}/src/Writable.ts (100%) create mode 100644 packages/core/src/caip222StyleSignature.ts rename packages/{react-app => core}/src/chains.ts (99%) rename packages/{react-app/src/gen/threecities/v1/v1_pb.ts => core/src/gen/threecities/v1/checkout_settings_pb.ts} (95%) create mode 100644 packages/core/src/gen/threecities/v1/logical_assets_pb.ts rename packages/{react-app => core}/src/getConfirmationsToWait.ts (100%) rename packages/{react-app => core}/src/hasOwnProperty.ts (100%) create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/isProduction.ts rename packages/{react-app => core}/src/logicalAssets.ts (99%) rename packages/{react-app => core}/src/logicalAssetsToTokens.ts (95%) rename packages/{react-app => core}/src/rpcUrls.ts (100%) rename packages/{react-app => core}/src/toUppercase.ts (100%) rename packages/{react-app => core}/src/tokens.ts (98%) create mode 100644 packages/core/tsconfig.json rename packages/react-app/{ => src}/images.d.ts (100%) delete mode 100644 packages/react-app/src/isProduction.ts create mode 100644 packages/verifier/.eslintrc.js create mode 100644 packages/verifier/.gitignore create mode 100644 packages/verifier/README.md create mode 100644 packages/verifier/package.json create mode 100644 packages/verifier/src/index.ts rename packages/{react-app/src/verifierPrototype.ts => verifier/src/verifyTransfer.ts} (93%) create mode 100644 packages/verifier/tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index 38a2a5f7..e846ae15 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -43,10 +43,6 @@ module.exports = { "error", { "paths": [ - { - "name": "ethers", - "message": "Please import from '@ethersproject/module' directly to support tree-shaking." - }, { "name": "@lingui/macro", "importNames": [ diff --git a/package.json b/package.json index 55d85c54..cb1f9c5b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,13 @@ "react" ], "private": true, + "dependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@tanstack/react-query": "^5.47", + "immer": "^10.1.1", + "viem": "2.17.2", + "wagmi": "2.10.9" + }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^7.14.1", "@typescript-eslint/parser": "^7.14.1", @@ -15,10 +22,11 @@ "typescript": "^5.5.2" }, "scripts": { - "lint": "yarn workspace @3cities/react-app lint && yarn workspace @3cities/eth-transfer-proxy lint", - "build": "yarn workspace @3cities/eth-transfer-proxy build && yarn workspace @3cities/react-app build", - "build:prod-test": "yarn workspace @3cities/eth-transfer-proxy build && yarn workspace @3cities/react-app build:prod-test", - "build:dev": "yarn workspace @3cities/eth-transfer-proxy build && yarn workspace @3cities/react-app build:dev", + "runTaskInAllPackages": "for package in core eth-transfer-proxy verifier react-app; do echo \"*** $TASK $package ***\" && yarn workspace @3cities/$package $TASK || exit 1; done", + "lint": "TASK=lint yarn runTaskInAllPackages", + "build": "TASK=build yarn runTaskInAllPackages", + "build:prod-test": "TASK='build:prod-test' yarn runTaskInAllPackages", + "build:dev": "TASK='build:dev' yarn runTaskInAllPackages", "start": "yarn workspace @3cities/react-app start" }, "workspaces": { diff --git a/packages/core/.eslintrc.js b/packages/core/.eslintrc.js new file mode 100644 index 00000000..e40864c8 --- /dev/null +++ b/packages/core/.eslintrc.js @@ -0,0 +1,11 @@ +// This package-specific .eslintrc.js file is automatically merged by `eslint` at runtime with our project's root .eslintrs.js https://eslint.org/docs/latest/use/configure/configuration-files#cascading-and-hierarchy +const path = require('path'); + +module.exports = { + "env": { + "es2023": true, + }, + "parserOptions": { + "project": path.resolve(__dirname, './tsconfig.json'), + }, +}; diff --git a/packages/core/.gitignore b/packages/core/.gitignore new file mode 100644 index 00000000..629ac705 --- /dev/null +++ b/packages/core/.gitignore @@ -0,0 +1,2 @@ +# build directories +dist diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..9389a594 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,6 @@ + +# Overview + +TODO + +# Usage diff --git a/packages/react-app/buf.gen.yaml b/packages/core/buf.gen.yaml similarity index 100% rename from packages/react-app/buf.gen.yaml rename to packages/core/buf.gen.yaml diff --git a/packages/react-app/buf.yaml b/packages/core/buf.yaml similarity index 100% rename from packages/react-app/buf.yaml rename to packages/core/buf.yaml diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..bf29becd --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@3cities/core", + "license": "MIT", + "version": "1.0.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist/cjs/**/*", + "dist/esm/**/*", + "dist/types/**/*" + ], + "exports": { + ".": { + "require": "./dist/cjs/index.js", + "import": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts" + }, + "./proto/checkout-settings": { + "require": "./dist/cjs/gen/threecities/v1/checkout_settings_pb.js", + "import": "./dist/esm/gen/threecities/v1/checkout_settings_pb.js", + "types": "./dist/types/gen/threecities/v1/checkout_settings_pb.d.ts" + }, + "./proto/logical-assets": { + "require": "./dist/cjs/gen/threecities/v1/logical_assets_pb.js", + "import": "./dist/esm/gen/threecities/v1/logical_assets_pb.js", + "types": "./dist/types/gen/threecities/v1/logical_assets_pb.d.ts" + } + }, + "devDependencies": { + "@bufbuild/buf": "^1.34.0", + "@bufbuild/protoc-gen-es": "^1.10.0", + "nodemon": "^3.1.4" + }, + "scripts": { + "clean:ts": "rm -rf ./dist", + "clean:cjs": "rm -rf ./dist/cjs", + "clean:esm": "rm -rf ./dist/esm", + "clean:types": "rm -rf ./dist/types", + "clean": "yarn clean:ts", + "build:cjs": "yarn clean:cjs && tsc --module commonjs --moduleResolution node --outDir ./dist/cjs --verbatimModuleSyntax false --sourceMap", + "build:esm": "yarn clean:esm && tsc --outDir ./dist/esm --sourceMap", + "build:types": "yarn clean:types && tsc --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", + "build:ts": "yarn build:cjs && yarn build:esm && yarn build:types", + "build": "yarn build:ts", + "build:dev": "yarn build # NB core has no dev build, but we include this so that the root build:dev task works for all packages. Clients must apply any env-specific settings, WARNING especially to pass appropriate env vars to core's isProduction", + "build:prod-test": "yarn build # NB core has no prod-test build, but we include this so that the root build:prod-test task works for all packages. Clients must apply any env-specific settings, WARNING especially to pass appropriate env vars to core's isProduction", + "gen": "npx buf generate proto", + "lint": "eslint . --max-warnings 0 && buf lint", + "prepack": "yarn && yarn build", + "start": "nodemon --watch 'src/**/*' --ext 'ts,proto' --exec 'yarn build'", + "test": "forge test", + "test:coverage": "forge coverage" + } +} diff --git a/packages/react-app/proto/threecities/v1/v1.proto b/packages/core/proto/threecities/v1/checkout_settings.proto similarity index 97% rename from packages/react-app/proto/threecities/v1/v1.proto rename to packages/core/proto/threecities/v1/checkout_settings.proto index 2edf990f..9c76beaa 100644 --- a/packages/react-app/proto/threecities/v1/v1.proto +++ b/packages/core/proto/threecities/v1/checkout_settings.proto @@ -1,17 +1,7 @@ syntax = "proto3"; package threecities.v1; // we follow a major versioning scheme for our proto files. All files in the threecities.v1 package are compatible with the first major version. In the future, new major versions like threecities.v2 will be introduced. At that point, threecities.v1 types will be deprecated. The v2 app layer will attempt v2 deserialization first, falling back to v1 only if the former fails. -// LogicalAssetTicker is the set of all logical asset tickers we -// support. Our app-layer LogicalAssetTicker types are automatically -// derived from this single authoritative definition in a typesafe -// manner. -enum LogicalAssetTicker { - LOGICAL_ASSET_TICKER_UNSPECIFIED = 0; - LOGICAL_ASSET_TICKER_ETH = 1; - LOGICAL_ASSET_TICKER_USD = 2; - LOGICAL_ASSET_TICKER_EUR = 3; - LOGICAL_ASSET_TICKER_CAD = 4; -} +import "threecities/v1/logical_assets.proto"; // CheckoutSettings is a flattened variant of our app-layer type // CheckoutSettings. This CheckoutSettings has been flattened into a diff --git a/packages/core/proto/threecities/v1/logical_assets.proto b/packages/core/proto/threecities/v1/logical_assets.proto new file mode 100644 index 00000000..a9ec2783 --- /dev/null +++ b/packages/core/proto/threecities/v1/logical_assets.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; +package threecities.v1; // we follow a major versioning scheme for our proto files. All files in the threecities.v1 package are compatible with the first major version. In the future, new major versions like threecities.v2 will be introduced. At that point, threecities.v1 types will be deprecated. The v2 app layer will attempt v2 deserialization first, falling back to v1 only if the former fails. + +// LogicalAssetTicker is the set of all logical asset tickers we +// support. Our app-layer LogicalAssetTicker types are automatically +// derived from this single authoritative definition in a typesafe +// manner. +enum LogicalAssetTicker { + LOGICAL_ASSET_TICKER_UNSPECIFIED = 0; + LOGICAL_ASSET_TICKER_ETH = 1; + LOGICAL_ASSET_TICKER_USD = 2; + LOGICAL_ASSET_TICKER_EUR = 3; + LOGICAL_ASSET_TICKER_CAD = 4; +} diff --git a/packages/react-app/src/ExchangeRates.ts b/packages/core/src/ExchangeRates.ts similarity index 98% rename from packages/react-app/src/ExchangeRates.ts rename to packages/core/src/ExchangeRates.ts index e1e32e17..0ac8842f 100644 --- a/packages/react-app/src/ExchangeRates.ts +++ b/packages/core/src/ExchangeRates.ts @@ -1,6 +1,6 @@ -import { DeepWritable } from "./Writable"; +import { type DeepWritable } from "./Writable"; import { hasOwnProperty } from "./hasOwnProperty"; -import { LogicalAssetTicker, parseLogicalAssetAmount } from "./logicalAssets"; +import { type LogicalAssetTicker, parseLogicalAssetAmount } from "./logicalAssets"; import { toUppercase } from "./toUppercase"; export type ExchangeRates = Readonly<{ diff --git a/packages/react-app/src/IntRange.ts b/packages/core/src/IntRange.ts similarity index 100% rename from packages/react-app/src/IntRange.ts rename to packages/core/src/IntRange.ts diff --git a/packages/react-app/src/NonEmptyArray.ts b/packages/core/src/NonEmptyArray.ts similarity index 100% rename from packages/react-app/src/NonEmptyArray.ts rename to packages/core/src/NonEmptyArray.ts diff --git a/packages/react-app/src/Optional.ts b/packages/core/src/Optional.ts similarity index 100% rename from packages/react-app/src/Optional.ts rename to packages/core/src/Optional.ts diff --git a/packages/react-app/src/Token.ts b/packages/core/src/Token.ts similarity index 98% rename from packages/react-app/src/Token.ts rename to packages/core/src/Token.ts index a99736a4..56c5aa17 100644 --- a/packages/react-app/src/Token.ts +++ b/packages/core/src/Token.ts @@ -1,4 +1,4 @@ -import { IntRange } from "./IntRange"; +import { type IntRange } from "./IntRange"; import { hasOwnProperty } from "./hasOwnProperty"; // Token is our type for fungible (ERC-20) tokens. An instance of diff --git a/packages/react-app/src/Writable.ts b/packages/core/src/Writable.ts similarity index 100% rename from packages/react-app/src/Writable.ts rename to packages/core/src/Writable.ts diff --git a/packages/core/src/caip222StyleSignature.ts b/packages/core/src/caip222StyleSignature.ts new file mode 100644 index 00000000..063512b7 --- /dev/null +++ b/packages/core/src/caip222StyleSignature.ts @@ -0,0 +1,41 @@ +import { mainnet, sepolia } from "./chains"; +import { isProduction } from "./isProduction"; + +// ERC-1271 +// isValidSignature(bytes32 hash, bytes signature) → bytes4 magicValue +export const erc1271SmartAccountAbi = [ + { + name: 'isValidSignature', + type: 'function', + stateMutability: 'view', + inputs: [ + { name: 'hash', type: 'bytes32' }, + { name: 'signature', type: 'bytes' }, + ], + outputs: [{ name: '', type: 'bytes4' }], + }, +] as const; + +export const erc1271MagicValue = "0x1626ba7e" as const; + +export const caip222StyleSignatureMessageDomain = { // EIP-712 message domain + name: '3cities', + version: '1', + // NB chainId is not needed for our message as we only want to prove sender address ownership (on any chain, assuming it then applies to all chains). Note that any chainId provided here is purely signed data and not actually used by any wallet to eg. switch to that chainId prior to signing +} as const; + +export const caip222StyleSignatureMessageTypes = { // EIP-712 message types + SenderAddress: [ + { name: 'senderAddress', type: 'address' }, + ], +} as const; + +export const caip222StyleSignatureMessagePrimaryType = 'SenderAddress' as const; // EIP-712 message primaryType + +export type Caip222StyleSignature = `0x${string}` | `eip1271-chainId-${number}`; // a successfully collected Caip222-style signature. `0x${string}` indicates an ordinary signature. `eip1271-chainId-${number}` indicates a smart contract wallet verified the message using eip1271 verification via a isValidSignature call on the provided chainId + +export type Caip222StyleMessageToSign = { + senderAddress: `0x${string}`; +}; + +export const chainIdOnWhichToSignMessagesAndVerifySignatures: number = isProduction ? mainnet.id : sepolia.id; // certain wallets generate different signatures depending on which chain is active at the time of message signing. To ensure that 3cities is later able to verify any generated signature, we only allow signature generation on a static sentinel chain (ie. the L1 in production). NB eip1271 onchain signatures are handled separately and use one of chains on which the eip1271 wallet exists diff --git a/packages/react-app/src/chains.ts b/packages/core/src/chains.ts similarity index 99% rename from packages/react-app/src/chains.ts rename to packages/core/src/chains.ts index e501f789..bf5d89ca 100644 --- a/packages/react-app/src/chains.ts +++ b/packages/core/src/chains.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line no-restricted-imports -- here is our single allowed use of importing from wagmi/chains, used to construct 3cities chains which are then exported to the rest of 3cities import { arbitrum, arbitrumNova, arbitrumSepolia, base, baseSepolia, blast, blastSepolia, immutableZkEvm, optimism, optimismSepolia, polygon, polygonAmoy, polygonZkEvm, polygonZkEvmCardona, scroll, scrollSepolia, sepolia, linea as wagmiLinea, lineaSepolia as wagmiLineaSepolia, mainnet as wagmiMainnet, mode as wagmiMode, taiko as wagmiTaiko, zkSyncSepoliaTestnet as wagmiZkSyncSeplia, zkSync, zora, zoraSepolia, type Chain } from 'wagmi/chains'; -import { NonEmptyArray } from './NonEmptyArray'; +import { type NonEmptyArray } from './NonEmptyArray'; import { isProduction } from './isProduction'; // *************************************************************** diff --git a/packages/react-app/src/gen/threecities/v1/v1_pb.ts b/packages/core/src/gen/threecities/v1/checkout_settings_pb.ts similarity index 95% rename from packages/react-app/src/gen/threecities/v1/v1_pb.ts rename to packages/core/src/gen/threecities/v1/checkout_settings_pb.ts index 18280f85..6bc09e2a 100644 --- a/packages/react-app/src/gen/threecities/v1/v1_pb.ts +++ b/packages/core/src/gen/threecities/v1/checkout_settings_pb.ts @@ -1,53 +1,11 @@ // @generated by protoc-gen-es v1.10.0 with parameter "target=ts" -// @generated from file threecities/v1/v1.proto (package threecities.v1, syntax proto3) +// @generated from file threecities/v1/checkout_settings.proto (package threecities.v1, syntax proto3) /* eslint-disable */ // @ts-nocheck import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; import { Message, proto3 } from "@bufbuild/protobuf"; - -/** - * LogicalAssetTicker is the set of all logical asset tickers we - * support. Our app-layer LogicalAssetTicker types are automatically - * derived from this single authoritative definition in a typesafe - * manner. - * - * @generated from enum threecities.v1.LogicalAssetTicker - */ -export enum LogicalAssetTicker { - /** - * @generated from enum value: LOGICAL_ASSET_TICKER_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: LOGICAL_ASSET_TICKER_ETH = 1; - */ - ETH = 1, - - /** - * @generated from enum value: LOGICAL_ASSET_TICKER_USD = 2; - */ - USD = 2, - - /** - * @generated from enum value: LOGICAL_ASSET_TICKER_EUR = 3; - */ - EUR = 3, - - /** - * @generated from enum value: LOGICAL_ASSET_TICKER_CAD = 4; - */ - CAD = 4, -} -// Retrieve enum metadata with: proto3.getEnumType(LogicalAssetTicker) -proto3.util.setEnumType(LogicalAssetTicker, "threecities.v1.LogicalAssetTicker", [ - { no: 0, name: "LOGICAL_ASSET_TICKER_UNSPECIFIED" }, - { no: 1, name: "LOGICAL_ASSET_TICKER_ETH" }, - { no: 2, name: "LOGICAL_ASSET_TICKER_USD" }, - { no: 3, name: "LOGICAL_ASSET_TICKER_EUR" }, - { no: 4, name: "LOGICAL_ASSET_TICKER_CAD" }, -]); +import { LogicalAssetTicker } from "./logical_assets_pb.js"; /** * MessageType is an enum to help clients differentiate between diff --git a/packages/core/src/gen/threecities/v1/logical_assets_pb.ts b/packages/core/src/gen/threecities/v1/logical_assets_pb.ts new file mode 100644 index 00000000..78bb487f --- /dev/null +++ b/packages/core/src/gen/threecities/v1/logical_assets_pb.ts @@ -0,0 +1,50 @@ +// @generated by protoc-gen-es v1.10.0 with parameter "target=ts" +// @generated from file threecities/v1/logical_assets.proto (package threecities.v1, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { proto3 } from "@bufbuild/protobuf"; + +/** + * LogicalAssetTicker is the set of all logical asset tickers we + * support. Our app-layer LogicalAssetTicker types are automatically + * derived from this single authoritative definition in a typesafe + * manner. + * + * @generated from enum threecities.v1.LogicalAssetTicker + */ +export enum LogicalAssetTicker { + /** + * @generated from enum value: LOGICAL_ASSET_TICKER_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: LOGICAL_ASSET_TICKER_ETH = 1; + */ + ETH = 1, + + /** + * @generated from enum value: LOGICAL_ASSET_TICKER_USD = 2; + */ + USD = 2, + + /** + * @generated from enum value: LOGICAL_ASSET_TICKER_EUR = 3; + */ + EUR = 3, + + /** + * @generated from enum value: LOGICAL_ASSET_TICKER_CAD = 4; + */ + CAD = 4, +} +// Retrieve enum metadata with: proto3.getEnumType(LogicalAssetTicker) +proto3.util.setEnumType(LogicalAssetTicker, "threecities.v1.LogicalAssetTicker", [ + { no: 0, name: "LOGICAL_ASSET_TICKER_UNSPECIFIED" }, + { no: 1, name: "LOGICAL_ASSET_TICKER_ETH" }, + { no: 2, name: "LOGICAL_ASSET_TICKER_USD" }, + { no: 3, name: "LOGICAL_ASSET_TICKER_EUR" }, + { no: 4, name: "LOGICAL_ASSET_TICKER_CAD" }, +]); + diff --git a/packages/react-app/src/getConfirmationsToWait.ts b/packages/core/src/getConfirmationsToWait.ts similarity index 100% rename from packages/react-app/src/getConfirmationsToWait.ts rename to packages/core/src/getConfirmationsToWait.ts diff --git a/packages/react-app/src/hasOwnProperty.ts b/packages/core/src/hasOwnProperty.ts similarity index 100% rename from packages/react-app/src/hasOwnProperty.ts rename to packages/core/src/hasOwnProperty.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..cddac894 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +import { type Caip222StyleMessageToSign, type Caip222StyleSignature, caip222StyleSignatureMessageDomain, caip222StyleSignatureMessagePrimaryType, caip222StyleSignatureMessageTypes, chainIdOnWhichToSignMessagesAndVerifySignatures, erc1271MagicValue, erc1271SmartAccountAbi } from "./caip222StyleSignature"; +import { type Chain, allSupportedChainIds, arbitrum, arbitrumNova, arbitrumSepolia, base, baseSepolia, blast, blastSepolia, chainsSupportedBy3cities, fluentTestnet, getChain, getSupportedChainName, immutableZkEvm, linea, lineaSepolia, mainnet, mode, optimism, optimismSepolia, polygon, polygonAmoy, polygonZkEvm, polygonZkEvmCardona, scroll, scrollSepolia, sepolia, taiko, zkSync, zkSyncSepolia, zora, zoraSepolia } from "./chains"; +import { type ExchangeRates, areExchangeRatesEqual, convert, mergeExchangeRates } from "./ExchangeRates"; +import { getConfirmationsToWait } from "./getConfirmationsToWait"; +import { hasOwnProperty, hasOwnPropertyOfType } from "./hasOwnProperty"; +import { type IntRange } from "./IntRange"; +import { isProduction } from "./isProduction"; +import { type LogicalAsset, type LogicalAssetTicker, addCanonicalFormatToLogicalAssetValue, addSymbolToLogicalAssetValue, addVerboseFormatToLogicalAssetValue, allLogicalAssetTickers, convertLogicalAssetUnits, defaultSmallAmountsPerLogicalAsset, getDecimalsToRenderForLogicalAssetTicker, getDefaultTruncateTrailingZeroesForLogicalAssetTicker, logicalAssetDecimals, logicalAssets, logicalAssetsByTicker, parseLogicalAssetAmount } from "./logicalAssets"; +import { getAllNativeCurrenciesAndTokensForLogicalAssetTicker, getDecimalsToRenderForTokenTicker, getDefaultTruncateTrailingZeroesForTokenTicker, getLogicalAssetTickerForTokenOrNativeCurrencyTicker, isTokenTickerSupportedByLogicalAsset } from "./logicalAssetsToTokens"; +import { type NonEmptyArray, ensureNonEmptyArray } from "./NonEmptyArray"; +import { alchemyHttpUrl, infuraHttpUrl } from './rpcUrls'; +import { type NativeCurrency, type Token, isNativeCurrency, isToken } from './Token'; +import { type TokenKey, allTokenTickers, getTokenByTokenKey, getTokenKey, isTokenSupported, nativeCurrencies, tokens } from "./tokens"; +import { toUppercase } from "./toUppercase"; +import { type DeepWritable, type Writable } from "./Writable"; + +// TODO consider adding more export subpaths (similar to @3cities/core/proto/checkout-settings), so not all exports are in the @3cities/core namespace, eg. @3cities/core/chains + +export { addCanonicalFormatToLogicalAssetValue, addSymbolToLogicalAssetValue, addVerboseFormatToLogicalAssetValue, alchemyHttpUrl, allLogicalAssetTickers, allSupportedChainIds, allTokenTickers, arbitrum, arbitrumNova, arbitrumSepolia, areExchangeRatesEqual, base, baseSepolia, blast, blastSepolia, caip222StyleSignatureMessageDomain, caip222StyleSignatureMessagePrimaryType, caip222StyleSignatureMessageTypes, chainIdOnWhichToSignMessagesAndVerifySignatures, chainsSupportedBy3cities, convert, convertLogicalAssetUnits, defaultSmallAmountsPerLogicalAsset, ensureNonEmptyArray, erc1271MagicValue, erc1271SmartAccountAbi, fluentTestnet, getAllNativeCurrenciesAndTokensForLogicalAssetTicker, getChain, getConfirmationsToWait, getDecimalsToRenderForLogicalAssetTicker, getDecimalsToRenderForTokenTicker, getDefaultTruncateTrailingZeroesForLogicalAssetTicker, getDefaultTruncateTrailingZeroesForTokenTicker, getLogicalAssetTickerForTokenOrNativeCurrencyTicker, getSupportedChainName, getTokenByTokenKey, getTokenKey, hasOwnProperty, hasOwnPropertyOfType, immutableZkEvm, infuraHttpUrl, isNativeCurrency, isProduction, isToken, isTokenSupported, isTokenTickerSupportedByLogicalAsset, linea, lineaSepolia, logicalAssetDecimals, logicalAssets, logicalAssetsByTicker, mainnet, mergeExchangeRates, mode, nativeCurrencies, optimism, optimismSepolia, parseLogicalAssetAmount, polygon, polygonAmoy, polygonZkEvm, polygonZkEvmCardona, scroll, scrollSepolia, sepolia, taiko, toUppercase, tokens, zkSync, zkSyncSepolia, zora, zoraSepolia, type Caip222StyleMessageToSign, type Caip222StyleSignature, type Chain, type DeepWritable, type ExchangeRates, type IntRange, type LogicalAsset, type LogicalAssetTicker, type NativeCurrency, type NonEmptyArray, type Token, type TokenKey, type Writable }; diff --git a/packages/core/src/isProduction.ts b/packages/core/src/isProduction.ts new file mode 100644 index 00000000..0f108b54 --- /dev/null +++ b/packages/core/src/isProduction.ts @@ -0,0 +1,5 @@ + +// TODO split isProduction into two different dimensions with four vars, 1. isMainnetDependencies (isTestnetDependencies = !isMainnetDependencies) and 2. isProductionEnvironment (isDevelopmentEnvironment = !isProductionEnvironment) --> see .env-cmdrc.js --> then, update some usages of today's isProduction to instead be isMainnetDependencies. For example, token/chains should be mainnet based on isMainnetDependencies and not isProduction. For example, the "Open Link" dev feature in Request Money should use isDevelopmentEnvironment --> this change allows our three environments (dev, prod-test, prod) to properly configure themselves (eg. today prod-test has isProduction=false but instead it should have isMainnetDependencies=false isProduction=true) + +// isProduction is true if and only if the app is being run in production mode, ie. against production dependencies, eg. to use blockchain mainnets instead of testnets. Note that whether or not the app is being run in production mode (ie. the truthiness of isProduction) is orthogonal to the type of build being used to run the app. The app may be run with isProduction==true while in built in development mode (eg. to debug locally against production dependencies), or the app may be run with isProduction==false in a production minified build (eg. to offer a production deployment against test dependencies for demo purposes) +export const isProduction: boolean = process.env['REACT_APP_IS_PRODUCTION'] === 'true'; diff --git a/packages/react-app/src/logicalAssets.ts b/packages/core/src/logicalAssets.ts similarity index 99% rename from packages/react-app/src/logicalAssets.ts rename to packages/core/src/logicalAssets.ts index 73e42391..15f8a773 100644 --- a/packages/react-app/src/logicalAssets.ts +++ b/packages/core/src/logicalAssets.ts @@ -1,6 +1,6 @@ import { parseUnits } from 'viem'; -import { IntRange } from "./IntRange"; -import { LogicalAssetTicker as LogicalAssetTickerPb } from "./gen/threecities/v1/v1_pb"; +import { type IntRange } from "./IntRange"; +import { LogicalAssetTicker as LogicalAssetTickerPb } from "./gen/threecities/v1/logical_assets_pb"; export type LogicalAssetTicker = Exclude; // here, we use the protobuf definition of logical asset tickers to be our single authoritative definition, and derive our app-layer logical asset ticker types from protobuf definitions in a typesafe manner. diff --git a/packages/react-app/src/logicalAssetsToTokens.ts b/packages/core/src/logicalAssetsToTokens.ts similarity index 95% rename from packages/react-app/src/logicalAssetsToTokens.ts rename to packages/core/src/logicalAssetsToTokens.ts index a163ff3c..2d88db23 100644 --- a/packages/react-app/src/logicalAssetsToTokens.ts +++ b/packages/core/src/logicalAssetsToTokens.ts @@ -1,6 +1,6 @@ -import { Immutable, castImmutable } from "immer"; -import { NativeCurrency, Token } from "./Token"; -import { LogicalAssetTicker, allLogicalAssetTickers, getDecimalsToRenderForLogicalAssetTicker, getDefaultTruncateTrailingZeroesForLogicalAssetTicker } from "./logicalAssets"; +import { type Immutable, castImmutable } from "immer"; +import { type NativeCurrency, type Token } from "./Token"; +import { type LogicalAssetTicker, allLogicalAssetTickers, getDecimalsToRenderForLogicalAssetTicker, getDefaultTruncateTrailingZeroesForLogicalAssetTicker } from "./logicalAssets"; import { tokensByTicker } from "./tokens"; // logicalAssetsToSupportedNativeCurrencyAndTokenTickers is our single diff --git a/packages/react-app/src/rpcUrls.ts b/packages/core/src/rpcUrls.ts similarity index 100% rename from packages/react-app/src/rpcUrls.ts rename to packages/core/src/rpcUrls.ts diff --git a/packages/react-app/src/toUppercase.ts b/packages/core/src/toUppercase.ts similarity index 100% rename from packages/react-app/src/toUppercase.ts rename to packages/core/src/toUppercase.ts diff --git a/packages/react-app/src/tokens.ts b/packages/core/src/tokens.ts similarity index 98% rename from packages/react-app/src/tokens.ts rename to packages/core/src/tokens.ts index fd81b51b..6b2cca11 100644 --- a/packages/react-app/src/tokens.ts +++ b/packages/core/src/tokens.ts @@ -1,6 +1,6 @@ -import { NonEmptyArray } from "./NonEmptyArray"; -import { Optional } from './Optional'; -import { NativeCurrency, Token, isToken } from "./Token"; +import { type NonEmptyArray } from "./NonEmptyArray"; +import { type Optional } from './Optional'; +import { isToken, type NativeCurrency, type Token } from "./Token"; import { allSupportedChainIds, arbitrum, arbitrumNova, arbitrumSepolia, base, baseSepolia, blast, blastSepolia, fluentTestnet, immutableZkEvm, linea, lineaSepolia, mainnet, mode, optimism, optimismSepolia, polygon, polygonAmoy, polygonZkEvm, polygonZkEvmCardona, scroll, scrollSepolia, sepolia, taiko, zkSync, zkSyncSepolia, zora, zoraSepolia, type Chain } from './chains'; import { isProduction } from "./isProduction"; import { toUppercase } from './toUppercase'; @@ -195,6 +195,7 @@ const polygonZkEvmWETH = token(polygonZkEvm, { name: 'Wrapped Ether', ticker: 'W const polygonZkEvmUSDC = token(polygonZkEvm, { name: 'USD Coin', ticker: 'USDC', contractAddress: '0xA8CE8aee21bC2A48a5EF670afCc9274C7bbbC035', decimals: 6 }); const polygonZkEvmUSDT = token(polygonZkEvm, { name: 'Tether USD', ticker: 'USDT', contractAddress: '0x1E4a5963aBFD975d8c9021ce480b42188849D41d', decimals: 6 }); const polygonZkEvmDAI = token(polygonZkEvm, { name: 'Dai', ticker: 'DAI', contractAddress: '0xC5015b9d9161Dca7e18e32f6f25C4aD850731Fd4' }); +// TODO there are two variants of DAI on polygonZkEvm. One is the above (called "Legacy DAI" by Quickswap). The other is 0x744C5860ba161b5316F7E80D9Ec415e2727e5bD5 (called "DAI.E" by Quickswap). It appears this latter is from the "zkEvm DAI bridge" - unsure why this exists https://github.com/BuildOnPolygon/zkevm-dai --> should we support both dais? --> there seems to be poor/zero liquidity for the new DAI.E const polygonZkEvmLUSD = token(polygonZkEvm, { name: 'Liquity USD', ticker: 'LUSD', contractAddress: '0x01E9A866c361eAd20Ab4e838287DD464dc67A50e' }); // polygonZkEvmCardona (settles on sepolia) diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..4429626e --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src/**/*.ts", + ], +} \ No newline at end of file diff --git a/packages/eth-transfer-proxy/package.json b/packages/eth-transfer-proxy/package.json index 18029bb5..33fdfd21 100644 --- a/packages/eth-transfer-proxy/package.json +++ b/packages/eth-transfer-proxy/package.json @@ -37,6 +37,8 @@ "build:types": "yarn clean:types && tsc --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", "build:ts": "yarn build:cjs && yarn build:esm && yarn build:types", "build": "yarn build:forge && yarn build:gen && yarn build:ts", + "build:dev": "yarn build # NB eth-transfer-proxy has no dev build, but we include this so that the root build:dev task works for all packages", + "build:prod-test": "yarn build # NB eth-transfer-proxy has no prod-test build, but we include this so that the root build:prod-test task works for all packages", "deploy:local": "PRIVATE_KEY='' MNEMONIC='' forge script script/Deploy.s.sol --broadcast --fork-url http://localhost:8545", "deploy:sepolia": "CHAIN=sepolia forge script script/Deploy.s.sol --broadcast --verify -vvv", "deploy:mainnet": "CHAIN=mainnet forge script script/Deploy.s.sol --broadcast --verify -vvv", @@ -51,7 +53,7 @@ "deploy:mode": "forge script script/Deploy.s.sol --broadcast --chain-id 34443 --rpc-url https://mainnet.mode.network --verify -vvv --verifier blockscout --verifier-url https://modescan.io/api\\?", "fmt": "forge fmt", "gas": "forge test --gas-report", - "lint": "forge fmt --check && yarn solhint {script,src,test}/**/*.sol && eslint --max-warnings 0", + "lint": "forge fmt --check && yarn solhint {script,src,test}/**/*.sol && eslint . --max-warnings 0", "prepack": "yarn && yarn build", "test": "forge test", "test:coverage": "forge coverage" diff --git a/packages/react-app/package.json b/packages/react-app/package.json index 57927d67..c61e38fc 100644 --- a/packages/react-app/package.json +++ b/packages/react-app/package.json @@ -18,12 +18,11 @@ ] }, "dependencies": { + "@3cities/core": "^1.0.0", "@3cities/eth-transfer-proxy": "^1.0.0", - "@bufbuild/protobuf": "^1.10.0", - "@tanstack/react-query": "^5.47", + "@3cities/verifier": "^1.0.0", "@walletconnect/ethereum-provider": "^2.13.3", "connectkit": "^1.8.2", - "immer": "^10.1.1", "localforage": "^1.10.0", "qr-code-styling": "^1.6.0-rc.1", "react": "^18.3.1", @@ -33,13 +32,9 @@ "react-router-dom": "^6.24.0", "react-use-clipboard": "^1.0.9", "sonner": "^1.5.0", - "use-immer": "^0.10.0", - "viem": "2.17.2", - "wagmi": "2.10.9" + "use-immer": "^0.10.0" }, "devDependencies": { - "@bufbuild/buf": "^1.34.0", - "@bufbuild/protoc-gen-es": "^1.10.0", "@testing-library/dom": "9.3.3", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", @@ -75,8 +70,7 @@ "browserslist": "npx browserslist", "depcheck": "depcheck", "eject": "react-scripts eject", - "gen": "npx buf generate proto", - "lint": "eslint . --max-warnings 0 && buf lint", + "lint": "eslint . --max-warnings 0", "start-raw": "REACT_APP_GIT_COMMIT=`git rev-parse --short HEAD` REACT_APP_GIT_COMMIT_DATE=`TZ=UTC0 git show --quiet --date=local --format='%cd UTC'` react-scripts start # run dev mode in the context of an env that's already been set; WARNING build-raw and start-raw each separately define the computation of env vars REACT_APP_GIT_COMMIT and REACT_APP_GIT_COMMIT_DATE, and these must be identical in both places", "start:dev": "yarn env-cmd -e dev yarn start-raw", "start:prod-test": "yarn env-cmd -e prod-test yarn start-raw", diff --git a/packages/react-app/src/About.tsx b/packages/react-app/src/About.tsx index d5a3425b..ede84eef 100644 --- a/packages/react-app/src/About.tsx +++ b/packages/react-app/src/About.tsx @@ -1,10 +1,7 @@ +import { allLogicalAssetTickers, allTokenTickers, chainsSupportedBy3cities, getAllNativeCurrenciesAndTokensForLogicalAssetTicker, isTokenTickerSupportedByLogicalAsset } from "@3cities/core"; import React from "react"; import { FaTelegram, FaTwitter } from "react-icons/fa6"; import { Link } from 'react-router-dom'; -import { chainsSupportedBy3cities } from "./chains"; -import { allLogicalAssetTickers } from "./logicalAssets"; -import { getAllNativeCurrenciesAndTokensForLogicalAssetTicker, isTokenTickerSupportedByLogicalAsset } from "./logicalAssetsToTokens"; -import { allTokenTickers } from "./tokens"; export const About: React.FC = () => { return
diff --git a/packages/react-app/src/ActiveDemoAccountContext.ts b/packages/react-app/src/ActiveDemoAccountContext.ts index 41e2b656..3047d143 100644 --- a/packages/react-app/src/ActiveDemoAccountContext.ts +++ b/packages/react-app/src/ActiveDemoAccountContext.ts @@ -1,5 +1,5 @@ import { createContext } from "react"; -import { Observer, makeObservableValue } from './observer'; +import { type Observer, makeObservableValue } from './observer'; // ActiveDemoAccountContext provides an observer for the currently // active demo account managed via DemoAccountProvider. If the observed diff --git a/packages/react-app/src/AddressContext.ts b/packages/react-app/src/AddressContext.ts index 3fe6ac3a..7c884713 100644 --- a/packages/react-app/src/AddressContext.ts +++ b/packages/react-app/src/AddressContext.ts @@ -1,5 +1,5 @@ -import { TokenBalance } from "./TokenBalance"; -import { TokenKey } from "./tokens"; +import { type TokenKey } from "@3cities/core"; +import { type TokenBalance } from "./TokenBalance"; export type AddressContext = Readonly<{ address: `0x${string}`; diff --git a/packages/react-app/src/BuildInfo.tsx b/packages/react-app/src/BuildInfo.tsx index adf2d44a..ad249b66 100644 --- a/packages/react-app/src/BuildInfo.tsx +++ b/packages/react-app/src/BuildInfo.tsx @@ -1,5 +1,5 @@ +import { isProduction } from "@3cities/core"; import React from "react"; -import { isProduction } from "./isProduction"; // buildInfo is a library that enables the app to include its own // build info. For example, this can be used to double-check which diff --git a/packages/react-app/src/CheckoutSettings.ts b/packages/react-app/src/CheckoutSettings.ts index 1bf9701f..c5944388 100644 --- a/packages/react-app/src/CheckoutSettings.ts +++ b/packages/react-app/src/CheckoutSettings.ts @@ -1,6 +1,6 @@ -import { ExchangeRates } from "./ExchangeRates"; -import { ProposedPayment } from "./Payment"; -import { StrategyPreferences } from "./StrategyPreferences"; +import { type ExchangeRates } from "@3cities/core"; +import { type ProposedPayment } from "./Payment"; +import { type StrategyPreferences } from "./StrategyPreferences"; // SenderNoteSettings describes whether the sender/buyer may optionally // or must provide a note to the receiver/seller. diff --git a/packages/react-app/src/CheckoutSettingsContext.ts b/packages/react-app/src/CheckoutSettingsContext.ts index ddd488f3..c56866e3 100644 --- a/packages/react-app/src/CheckoutSettingsContext.ts +++ b/packages/react-app/src/CheckoutSettingsContext.ts @@ -1,6 +1,6 @@ +import { hasOwnPropertyOfType } from "@3cities/core"; import React from "react"; -import { CheckoutSettings } from "./CheckoutSettings"; -import { hasOwnPropertyOfType } from "./hasOwnProperty"; +import { type CheckoutSettings } from "./CheckoutSettings"; // CheckoutSettingsRequiresPassword represents the global contextual // CheckoutSettings requiring a password to proceed. The client should diff --git a/packages/react-app/src/CheckoutSettingsProvider.tsx b/packages/react-app/src/CheckoutSettingsProvider.tsx index c857358d..13730b73 100644 --- a/packages/react-app/src/CheckoutSettingsProvider.tsx +++ b/packages/react-app/src/CheckoutSettingsProvider.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useMemo, useState } from "react"; import { Outlet, useMatches, useSearchParams } from "react-router-dom"; -import { CheckoutSettings } from "./CheckoutSettings"; -import { CheckoutSettingsContext, CheckoutSettingsRequiresPassword } from "./CheckoutSettingsContext"; -import { MaybeCheckoutSettings, deserializeCheckoutSettingsUnknownMessageType, deserializeCheckoutSettingsWithEncryption, deserializeCheckoutSettingsWithSignature } from "./serialize"; +import { type CheckoutSettings } from "./CheckoutSettings"; +import { CheckoutSettingsContext, type CheckoutSettingsRequiresPassword } from "./CheckoutSettingsContext"; +import { type MaybeCheckoutSettings, deserializeCheckoutSettingsUnknownMessageType, deserializeCheckoutSettingsWithEncryption, deserializeCheckoutSettingsWithSignature } from "./serialize"; import { useEffectSkipFirst } from "./useEffectSkipFirst"; // TODO for 'CheckoutSettingsHasSignatureToVerify', the benefit of signatures vs encryption is that the cleartext CheckoutSettings is included in the CheckoutSettingsSigned. So, we might leverage that cleartext eg. by showing certain unverified payment details before the password is typed in. Or perhaps to bypass the password and see Pay screen with a big red "UNVERIFIED PAY LINK". One way to do this is to have MaybeCheckoutSettings return something like `type CheckoutSettingsUnverified = { checkoutSettings: CheckoutSettings }` instead of `CheckoutSettingsHasSignatureToVerify` and then the downstream CheckoutSettingsRequiresPassword could have something like `requirement: { kind: 'needToDecrypt' } | { kind: 'needToVerifySignature'; skipVerification: () => void; }` and then the client could call skipVerification. And then for Pay to detect skipped verification and show a warning banner, CheckoutSettingsContext could have `CheckoutSettings | CheckoutSettingsRequiresPassword | CheckoutSettingsUnverified` diff --git a/packages/react-app/src/ConnectedAccountContextObserverContext.ts b/packages/react-app/src/ConnectedAccountContextObserverContext.ts index 61b58997..42a4a53c 100644 --- a/packages/react-app/src/ConnectedAccountContextObserverContext.ts +++ b/packages/react-app/src/ConnectedAccountContextObserverContext.ts @@ -1,6 +1,6 @@ import React from 'react'; -import { AddressContext } from './AddressContext'; -import { Observer, makeObservableValue } from './observer'; +import { type AddressContext } from './AddressContext'; +import { type Observer, makeObservableValue } from './observer'; // ConnectedAccountContextObserverContext provides an observer for the // connected account's AddressContext, which is automatically kept diff --git a/packages/react-app/src/ConnectedAccountContextObserverProvider.tsx b/packages/react-app/src/ConnectedAccountContextObserverProvider.tsx index e622d0f9..e8776dcf 100644 --- a/packages/react-app/src/ConnectedAccountContextObserverProvider.tsx +++ b/packages/react-app/src/ConnectedAccountContextObserverProvider.tsx @@ -1,15 +1,16 @@ +import { type NativeCurrency, type Token, getTokenKey, nativeCurrencies, tokens } from '@3cities/core'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useImmer } from 'use-immer'; import { useAccount } from 'wagmi'; -import { AddressContext, emptyAddressContext } from './AddressContext'; +import { type AddressContext, emptyAddressContext } from './AddressContext'; import { ConnectedAccountContextObserverContext } from './ConnectedAccountContextObserverContext'; -import { NativeCurrency, Token } from './Token'; -import { TokenBalance, isDust } from './TokenBalance'; -import { ObservableValueUpdater, makeObservableValue } from './observer'; -import { getTokenKey, nativeCurrencies, tokens } from './tokens'; +import { type TokenBalance, isDust } from './TokenBalance'; +import { type ObservableValueUpdater, makeObservableValue } from './observer'; import { useLiveNativeCurrencyBalance } from './useLiveNativeCurrencyBalance'; import { useLiveTokenBalance } from './useLiveTokenBalance'; +// TODO there's a performance bug in ConnectedAccountContextObserverProvider in that ConnectedAccountContextUpdaterInner is both the container for the AddressContext maintained as state as well as the parent for the TokenBalanceUpdater and NativeCurrencyBalanceUpdater components. The result is that every time _any_ token or native currency balance updates, the AddressContext is updated as state, and this causes an unconditional rerender in _all_ updater child components since their parent rerendered due to state update. The solution is to employ a 2nd layer of observer indirection similar to ExchangeRatesUpdater, such that the updater components are not descendants of the component storing AddressContext as state + type ConnectedAccountContextObserverProviderProps = { children?: React.ReactNode; } diff --git a/packages/react-app/src/CurrencyAmountInput.tsx b/packages/react-app/src/CurrencyAmountInput.tsx index 8d68b0d6..1dec6cb9 100644 --- a/packages/react-app/src/CurrencyAmountInput.tsx +++ b/packages/react-app/src/CurrencyAmountInput.tsx @@ -1,9 +1,8 @@ +import { type ExchangeRates, type LogicalAsset, type LogicalAssetTicker, convert, getDecimalsToRenderForLogicalAssetTicker, logicalAssetsByTicker, parseLogicalAssetAmount } from "@3cities/core"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import CurrencyInput from "react-currency-input-field"; import { FaTimesCircle } from "react-icons/fa"; -import { ExchangeRates, convert } from "./ExchangeRates"; import { RenderLogicalAssetAmount } from "./RenderLogicalAssetAmount"; -import { LogicalAsset, LogicalAssetTicker, getDecimalsToRenderForLogicalAssetTicker, logicalAssetsByTicker, parseLogicalAssetAmount } from "./logicalAssets"; import { useExchangeRates } from "./useExchangeRates"; interface CurrencyAmountInputProps { diff --git a/packages/react-app/src/DemoAccountProvider.tsx b/packages/react-app/src/DemoAccountProvider.tsx index 37867b90..0c65e97f 100644 --- a/packages/react-app/src/DemoAccountProvider.tsx +++ b/packages/react-app/src/DemoAccountProvider.tsx @@ -1,10 +1,10 @@ -import React, { FC, useCallback, useEffect, useMemo, useState } from "react"; +import React, { type FC, useCallback, useEffect, useMemo, useState } from "react"; import { useSearchParams } from 'react-router-dom'; import { isAddress } from "viem"; import { useAccount, useAccountEffect, useConnect, useDisconnect } from 'wagmi'; import { mock } from 'wagmi/connectors'; import { ActiveDemoAccountContext } from './ActiveDemoAccountContext'; -import { ObservableValueUpdater, Observer, makeObservableValue } from './observer'; +import { type ObservableValueUpdater, type Observer, makeObservableValue } from './observer'; import { useEnsAddress } from './useEnsAddress'; type DemoAccountProviderProps = { diff --git a/packages/react-app/src/ExchangeRatesContext.ts b/packages/react-app/src/ExchangeRatesContext.ts index 4357544f..48bf230e 100644 --- a/packages/react-app/src/ExchangeRatesContext.ts +++ b/packages/react-app/src/ExchangeRatesContext.ts @@ -1,6 +1,6 @@ +import { type ExchangeRates } from '@3cities/core'; import React from 'react'; -import { ExchangeRates } from './ExchangeRates'; -import { Observer, makeObservableValue } from './observer'; +import { type Observer, makeObservableValue } from './observer'; // ExchangeRatesContext provides an observer for the global exchange // rates, which is automatically kept up-to-date with the latest diff --git a/packages/react-app/src/ExchangeRatesProvider.tsx b/packages/react-app/src/ExchangeRatesProvider.tsx index 10e68cf0..155bb5ca 100644 --- a/packages/react-app/src/ExchangeRatesProvider.tsx +++ b/packages/react-app/src/ExchangeRatesProvider.tsx @@ -1,14 +1,10 @@ +import { type DeepWritable, type ExchangeRates, areExchangeRatesEqual, isProduction, mainnet, sepolia, toUppercase } from "@3cities/core"; import React, { useEffect, useState } from 'react'; import { useImmer } from 'use-immer'; import { serialize } from 'wagmi'; import { readContracts } from 'wagmi/actions'; -import { ExchangeRates, areExchangeRatesEqual } from './ExchangeRates'; import { ExchangeRatesContext } from './ExchangeRatesContext'; -import { DeepWritable } from './Writable'; -import { mainnet, sepolia } from './chains'; -import { isProduction } from './isProduction'; -import { ObservableValue, ObservableValueUpdater, ObservableValueUpdaterWithCurrentValue, Observer, makeObservableValue } from './observer'; -import { toUppercase } from './toUppercase'; +import { type ObservableValue, type ObservableValueUpdater, type ObservableValueUpdaterWithCurrentValue, type Observer, makeObservableValue } from './observer'; import useDebounce from './useDebounce'; import { useIsPageVisibleOrRecentlyVisible } from './useIsPageVisibleOrRecentlyVisible'; import { wagmiConfig } from './wagmiClient'; diff --git a/packages/react-app/src/GlobalErrorBoundary.tsx b/packages/react-app/src/GlobalErrorBoundary.tsx index cd94c953..478fde12 100644 --- a/packages/react-app/src/GlobalErrorBoundary.tsx +++ b/packages/react-app/src/GlobalErrorBoundary.tsx @@ -1,8 +1,8 @@ +import { hasOwnPropertyOfType } from "@3cities/core"; import React from "react"; import { Link, useRouteError } from "react-router-dom"; import useClipboard from "react-use-clipboard"; import { serialize, useAccount } from "wagmi"; -import { hasOwnPropertyOfType } from "./hasOwnProperty"; const styleOuterDiv: React.CSSProperties = { position: 'absolute', diff --git a/packages/react-app/src/GlobalProviders.tsx b/packages/react-app/src/GlobalProviders.tsx index 8ff2cc9d..02a4da09 100644 --- a/packages/react-app/src/GlobalProviders.tsx +++ b/packages/react-app/src/GlobalProviders.tsx @@ -1,6 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ConnectKitProvider } from "connectkit"; -import { ConnectKitOptions } from "connectkit/build/types"; import React from "react"; import { Outlet, ScrollRestoration } from "react-router-dom"; import { Toaster } from 'sonner'; @@ -23,7 +22,7 @@ const queryClient = new QueryClient({ } }); -const connectKitOptions: ConnectKitOptions = { +const connectKitOptions/* : ConnectKitOptions --> type ConnectKitOptions no longer imports successfully after we switched from tsconfig moduleResolution "node" to "bundler" --> TODO fix it, perhaps by asking connectkit to make their export compatible with "bundler" */ = { hideBalance: true, walletConnectName: "WalletConnect", // default is "Other Wallets" which I find confusing because anybody who knows they can scan a qr code from mobile will most likely be looking for the name "WalletConnect" bufferPolyfill: false, // disable connectkit's Buffer polyfill because we have our own diff --git a/packages/react-app/src/IsPageVisibleOrRecentlyVisibleContext.ts b/packages/react-app/src/IsPageVisibleOrRecentlyVisibleContext.ts index 5b9c6468..3c597267 100644 --- a/packages/react-app/src/IsPageVisibleOrRecentlyVisibleContext.ts +++ b/packages/react-app/src/IsPageVisibleOrRecentlyVisibleContext.ts @@ -1,5 +1,5 @@ import { createContext } from "react"; -import { Observer, makeObservableValue } from './observer'; +import { type Observer, makeObservableValue } from './observer'; // IsPageVisibleOrRecentlyVisibleContext provides an observer for the // global flag indicating whether or not the page was recently visible. diff --git a/packages/react-app/src/IsPageVisibleOrRecentlyVisibleProvider.tsx b/packages/react-app/src/IsPageVisibleOrRecentlyVisibleProvider.tsx index b64005e3..1f3363b8 100644 --- a/packages/react-app/src/IsPageVisibleOrRecentlyVisibleProvider.tsx +++ b/packages/react-app/src/IsPageVisibleOrRecentlyVisibleProvider.tsx @@ -1,6 +1,6 @@ -import React, { FC, useEffect, useState } from 'react'; +import React, { type FC, useEffect, useState } from 'react'; import { IsPageVisibleOrRecentlyVisibleContext } from './IsPageVisibleOrRecentlyVisibleContext'; -import { ObservableValueUpdater, Observer, makeObservableValue } from './observer'; +import { type ObservableValueUpdater, type Observer, makeObservableValue } from './observer'; import useDebounce from './useDebounce'; type IsPageVisibleOrRecentlyVisibleProviderProps = { diff --git a/packages/react-app/src/MainWrapper.tsx b/packages/react-app/src/MainWrapper.tsx index de6ce76b..63408957 100644 --- a/packages/react-app/src/MainWrapper.tsx +++ b/packages/react-app/src/MainWrapper.tsx @@ -1,7 +1,7 @@ import React, { useContext } from "react"; import { FaHandHoldingUsd, FaHome, FaQuestionCircle } from "react-icons/fa"; import { FaTelegram, FaTwitter } from "react-icons/fa6"; -import { NavLink, NavLinkProps, Outlet } from "react-router-dom"; +import { NavLink, type NavLinkProps, Outlet } from "react-router-dom"; import { ConnectWalletButtonCustom } from "./ConnectWalletButton"; import { HideFooterOnMobileContext } from "./HideFooter"; import { Wordmark } from "./Wordmark"; diff --git a/packages/react-app/src/MaybeUserPage.tsx b/packages/react-app/src/MaybeUserPage.tsx index 12c1042b..b2686e2b 100644 --- a/packages/react-app/src/MaybeUserPage.tsx +++ b/packages/react-app/src/MaybeUserPage.tsx @@ -1,14 +1,14 @@ +import { parseLogicalAssetAmount } from "@3cities/core"; import React from "react"; import { useParams } from "react-router-dom"; import { isAddress } from "viem"; -import { AddressOrEnsName } from "./AddressOrEnsName"; -import { CheckoutSettings } from "./CheckoutSettings"; +import { type AddressOrEnsName } from "./AddressOrEnsName"; +import { type CheckoutSettings } from "./CheckoutSettings"; import { CheckoutSettingsContext } from "./CheckoutSettingsContext"; import { ConversionWrapperWithChildren } from "./ConversionWrapper"; +import { mightBeAnEnsName } from "./mightBeAnEnsName"; import { Pay } from "./Pay"; import { PrimaryWithSecondaries } from "./PrimaryWithSecondaries"; -import { parseLogicalAssetAmount } from "./logicalAssets"; -import { mightBeAnEnsName } from "./mightBeAnEnsName"; export const MaybeUserPage: React.FC = () => { // const { isConnected } = useAccount(); diff --git a/packages/react-app/src/Pay.tsx b/packages/react-app/src/Pay.tsx index 754a4783..8622bead 100644 --- a/packages/react-app/src/Pay.tsx +++ b/packages/react-app/src/Pay.tsx @@ -1,3 +1,4 @@ +import { type ExchangeRates, type LogicalAssetTicker, convert, defaultSmallAmountsPerLogicalAsset, getChain, getLogicalAssetTickerForTokenOrNativeCurrencyTicker, getSupportedChainName, getTokenKey, isNativeCurrency, parseLogicalAssetAmount } from "@3cities/core"; import { getETHTransferProxyContractAddress } from "@3cities/eth-transfer-proxy"; import React, { useCallback, useEffect, useMemo, useState, useTransition } from "react"; import { FaEye } from "react-icons/fa"; @@ -5,29 +6,23 @@ import { Link } from "react-router-dom"; import useClipboard from "react-use-clipboard"; import { toast } from "sonner"; import { serialize, useAccount } from "wagmi"; -import { CheckoutSettings } from "./CheckoutSettings"; -import { CheckoutSettingsRequiresPassword, isCheckoutSettingsRequiresPassword } from "./CheckoutSettingsContext"; +import { getBlockExplorerUrlForAddress, getBlockExplorerUrlForTransaction } from "./blockExplorerUrls"; +import { type CheckoutSettings } from "./CheckoutSettings"; +import { type CheckoutSettingsRequiresPassword, isCheckoutSettingsRequiresPassword } from "./CheckoutSettingsContext"; import { ConnectWalletButton } from "./ConnectWalletButton"; import { CurrencyAmountInput } from "./CurrencyAmountInput"; -import { ExchangeRates, convert } from "./ExchangeRates"; import { ExternalLink } from "./ExternalLink"; -import { Payment, PaymentWithFixedAmount, ProposedPaymentWithFixedAmount, ProposedPaymentWithReceiverAddress, acceptProposedPayment, isPaymentWithFixedAmount, isProposedPaymentWithFixedAmount } from "./Payment"; +import { type IframeMessage, closeIframe, isRunningInAStandaloneWindow, isRunningInAnIframe, notifyParentWindowOfSuccessfulCheckout, notifyParentWindowOfTransactionSigned } from "./iframe"; +import { type Payment, type PaymentWithFixedAmount, type ProposedPaymentWithFixedAmount, type ProposedPaymentWithReceiverAddress, acceptProposedPayment, isPaymentWithFixedAmount, isProposedPaymentWithFixedAmount } from "./Payment"; import { PrimaryWithSecondaries } from "./PrimaryWithSecondaries"; import QRCode from "./QRCode"; import { RenderLogicalAssetAmount, renderLogicalAssetAmount } from "./RenderLogicalAssetAmount"; import { RenderTokenBalance } from "./RenderTokenBalance"; import { RenderTokenTransfer } from "./RenderTokenTransfer"; +import { type ProposedStrategy, type Strategy, getProposedStrategiesForProposedPayment, getStrategiesForPayment } from "./strategies"; import { ToggleSwitch } from "./ToggleSwitch"; -import { isNativeCurrency } from "./Token"; -import { getBlockExplorerUrlForAddress, getBlockExplorerUrlForTransaction } from "./blockExplorerUrls"; -import { getChain, getSupportedChainName } from "./chains"; -import { IframeMessage, closeIframe, isRunningInAStandaloneWindow, isRunningInAnIframe, notifyParentWindowOfSuccessfulCheckout, notifyParentWindowOfTransactionSigned } from "./iframe"; -import { LogicalAssetTicker, defaultSmallAmountsPerLogicalAsset, parseLogicalAssetAmount } from "./logicalAssets"; -import { getLogicalAssetTickerForTokenOrNativeCurrencyTicker } from "./logicalAssetsToTokens"; -import { ProposedStrategy, Strategy, getProposedStrategiesForProposedPayment, getStrategiesForPayment } from "./strategies"; -import { TokenTransfer } from "./tokenTransfer"; -import { getTokenKey } from "./tokens"; -import { ExecuteTokenTransferButton, ExecuteTokenTransferButtonProps, ExecuteTokenTransferButtonStatus, TransactionFeeUnaffordableError } from "./transactions"; +import { type TokenTransfer } from "./tokenTransfer"; +import { ExecuteTokenTransferButton, type ExecuteTokenTransferButtonProps, type ExecuteTokenTransferButtonStatus, TransactionFeeUnaffordableError } from "./transactions"; import { truncateEnsName, truncateEthAddress } from "./truncateAddress"; import { useActiveDemoAccount } from "./useActiveDemoAccount"; import { useBestStrategy } from "./useBestStrategy"; diff --git a/packages/react-app/src/Payment.ts b/packages/react-app/src/Payment.ts index 59426000..f274286c 100644 --- a/packages/react-app/src/Payment.ts +++ b/packages/react-app/src/Payment.ts @@ -1,9 +1,9 @@ -import { AddressOrEnsName } from "./AddressOrEnsName"; -import { Narrow } from "./Narrow"; -import { PartialFor } from "./PartialFor"; +import { hasOwnPropertyOfType } from "@3cities/core"; +import { type AddressOrEnsName } from "./AddressOrEnsName"; +import { type LogicalAssetTicker } from "@3cities/core"; +import { type Narrow } from "./Narrow"; +import { type PartialFor } from "./PartialFor"; import { PrimaryWithSecondaries } from "./PrimaryWithSecondaries"; -import { hasOwnPropertyOfType } from "./hasOwnProperty"; -import { LogicalAssetTicker } from "./logicalAssets"; // TODO consider adding an TokenAmount(token: NativeCurrency | Token, amount: bigint) and LogicalAssetAmount(lat: LogicalAssetTicker, amount: bigint) for contexts where the amount should be coupled to its token, with eg. helper functions to convert amounts to other tokens/logical assets given exchange rates, or add/subtract amounts if they share a token/lat --> in some contexts, coupling the token to its amount can lead to undesirable or representable illegal states, such as in Payment where we want to separate the amount from the logical asset because "pay what you want" mode doesn't have a fixed amount. But in many other cases, combining the token/lat with its amount can lead to safer code because the amount is never detached from its token/decimals --> NB for these abstractions to be compatible with eventually supporting arbitrary tokens beyond our manifest of supported tokens, perhaps they should include the Token/NativeCurrency objects directly instead of their tickers, which prevents having to map the ticker to the Token using the global manifest, which is impossible when the Token was dynamically constructed from tokenlist, eg. user has UNI in their wallet. // TODO we also need much better and safer logical asset/token/native currency conversion facilities. Today, the ExchangeRates's convert will apply an exchange rate, but ignores decimals. We need type-safe amounts whose types and conversion facilities come from the modules to which they are related. For example, types and facilities to convert between logical asset amounts should be in logicalAssets.ts. Types and facilities to convert tokens/native currencies and logical assets that support them should be in logicalAssetsToTokens.ts. Types and facilities to convert between different tokens/native currencies/logical assets should be in exchange rates. No API should ever return an answer that needs further processing to become sensical. Eg. you should be able to pass convert a USDC amount and ask it to convert it to an ETH amount, and it should handle the conversion rate as well as the decimals. Btw, ETH is the only logical asset that's also a native currency, so that's another reason it's good for logical assets to use 18 decimals: it makes logical ETH amounts equivalent to ETH native currency amounts, supporting these APIs. diff --git a/packages/react-app/src/QRCode.tsx b/packages/react-app/src/QRCode.tsx index 250a40e1..fc16caee 100644 --- a/packages/react-app/src/QRCode.tsx +++ b/packages/react-app/src/QRCode.tsx @@ -1,4 +1,4 @@ -import QRCodeStyling, { Options } from 'qr-code-styling'; +import QRCodeStyling, { type Options } from 'qr-code-styling'; import React, { useEffect, useRef, useState } from 'react'; import { Spinner } from './Spinner'; import useWindowSize from './useWindowSize'; diff --git a/packages/react-app/src/RenderLiveTokenBalance.tsx b/packages/react-app/src/RenderLiveTokenBalance.tsx index 047e9675..f44b10fb 100644 --- a/packages/react-app/src/RenderLiveTokenBalance.tsx +++ b/packages/react-app/src/RenderLiveTokenBalance.tsx @@ -1,6 +1,6 @@ import React from "react"; import { RenderRawTokenBalance } from "./RenderRawTokenBalance"; -import { NativeCurrency, Token, isToken } from "./Token"; +import { type NativeCurrency, type Token, isToken } from "@3cities/core"; import { useLiveNativeCurrencyBalance } from "./useLiveNativeCurrencyBalance"; import { useLiveTokenBalance } from "./useLiveTokenBalance"; diff --git a/packages/react-app/src/RenderLogicalAssetAmount.tsx b/packages/react-app/src/RenderLogicalAssetAmount.tsx index b3b92f31..a35c26d6 100644 --- a/packages/react-app/src/RenderLogicalAssetAmount.tsx +++ b/packages/react-app/src/RenderLogicalAssetAmount.tsx @@ -1,7 +1,7 @@ +import { addCanonicalFormatToLogicalAssetValue, getDecimalsToRenderForLogicalAssetTicker, getDefaultTruncateTrailingZeroesForLogicalAssetTicker, logicalAssetDecimals, type LogicalAssetTicker } from "@3cities/core"; import React from "react"; import { formatUnits } from "viem"; -import { formatFloat, FormatFloatOpts } from "./formatFloat"; -import { addCanonicalFormatToLogicalAssetValue, getDecimalsToRenderForLogicalAssetTicker, getDefaultTruncateTrailingZeroesForLogicalAssetTicker, logicalAssetDecimals, LogicalAssetTicker } from "./logicalAssets"; +import { formatFloat, type FormatFloatOpts } from "./formatFloat"; type RenderLogicalAssetAmountProps = { logicalAssetTicker: LogicalAssetTicker; // logical asset ticker of the amount to be rendered diff --git a/packages/react-app/src/RenderRawTokenBalance.tsx b/packages/react-app/src/RenderRawTokenBalance.tsx index 4838d8b7..284cf390 100644 --- a/packages/react-app/src/RenderRawTokenBalance.tsx +++ b/packages/react-app/src/RenderRawTokenBalance.tsx @@ -1,9 +1,7 @@ -import { formatUnits } from "viem"; +import { type NativeCurrency, type Token, getDecimalsToRenderForTokenTicker, getDefaultTruncateTrailingZeroesForTokenTicker, getSupportedChainName } from "@3cities/core"; import React from "react"; -import { NativeCurrency, Token } from "./Token"; -import { getSupportedChainName } from "./chains"; -import { FormatFloatOpts, formatFloat } from "./formatFloat"; -import { getDecimalsToRenderForTokenTicker, getDefaultTruncateTrailingZeroesForTokenTicker } from "./logicalAssetsToTokens"; +import { formatUnits } from "viem"; +import { type FormatFloatOpts, formatFloat } from "./formatFloat"; export type RenderRawTokenBalanceProps = { balance: bigint | undefined; // token balance to render WARNING must be denominated in full-precision units of the passed nativeCurrencyOrToken diff --git a/packages/react-app/src/RenderTokenBalance.tsx b/packages/react-app/src/RenderTokenBalance.tsx index 8a84f18f..04995a8e 100644 --- a/packages/react-app/src/RenderTokenBalance.tsx +++ b/packages/react-app/src/RenderTokenBalance.tsx @@ -1,7 +1,7 @@ +import { getTokenByTokenKey } from "@3cities/core"; import React from "react"; -import { RenderRawTokenBalance, RenderRawTokenBalanceProps } from "./RenderRawTokenBalance"; -import { TokenBalance } from "./TokenBalance"; -import { getTokenByTokenKey } from "./tokens"; +import { RenderRawTokenBalance, type RenderRawTokenBalanceProps } from "./RenderRawTokenBalance"; +import { type TokenBalance } from "./TokenBalance"; type RenderTokenBalanceProps = { tb: TokenBalance; diff --git a/packages/react-app/src/RenderTokenTransfer.tsx b/packages/react-app/src/RenderTokenTransfer.tsx index 2bfb9059..e2aeb1e8 100644 --- a/packages/react-app/src/RenderTokenTransfer.tsx +++ b/packages/react-app/src/RenderTokenTransfer.tsx @@ -1,9 +1,8 @@ -import { formatUnits } from "viem"; +import { getDecimalsToRenderForTokenTicker, getDefaultTruncateTrailingZeroesForTokenTicker, getSupportedChainName } from "@3cities/core"; import React from "react"; -import { getSupportedChainName } from "./chains"; -import { FormatFloatOpts, formatFloat } from "./formatFloat"; -import { getDecimalsToRenderForTokenTicker, getDefaultTruncateTrailingZeroesForTokenTicker } from "./logicalAssetsToTokens"; -import { ProposedTokenTransfer, TokenTransfer } from "./tokenTransfer"; +import { formatUnits } from "viem"; +import { type FormatFloatOpts, formatFloat } from "./formatFloat"; +import { type ProposedTokenTransfer, type TokenTransfer } from "./tokenTransfer"; type RenderTokenTransferProps = { tt: TokenTransfer; diff --git a/packages/react-app/src/RequestMoney.tsx b/packages/react-app/src/RequestMoney.tsx index f412fb24..3fa5e039 100644 --- a/packages/react-app/src/RequestMoney.tsx +++ b/packages/react-app/src/RequestMoney.tsx @@ -1,3 +1,4 @@ +import { type LogicalAssetTicker, allSupportedChainIds, allTokenTickers, getSupportedChainName, isProduction, isTokenTickerSupportedByLogicalAsset, parseLogicalAssetAmount } from "@3cities/core"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { FaCheckCircle, FaExclamationCircle, FaInfoCircle, FaRegCopy, FaTimesCircle } from "react-icons/fa"; import useClipboard from "react-use-clipboard"; @@ -5,26 +6,21 @@ import { toast } from "sonner"; import { useImmer } from "use-immer"; import { isAddress } from "viem"; import { useAccount, useAccountEffect, useDisconnect } from "wagmi"; -import { CheckoutSettings, SuccessActionRedirect } from "./CheckoutSettings"; +import { type CheckoutSettings, type SuccessActionRedirect } from "./CheckoutSettings"; import { serializedCheckoutSettingsUrlParam } from "./CheckoutSettingsProvider"; import { ConnectWalletButtonCustom } from "./ConnectWalletButton"; import { CurrencyAmountInput } from "./CurrencyAmountInput"; +import { isLikelyAnEnsName } from "./isLikelyAnEnsName"; import { Modal, useModal } from "./Modal"; -import { PaymentMode, ProposedPayment, isPaymentModeWithFixedAmount, isProposedPaymentWithFixedAmount } from "./Payment"; +import { type PaymentMode, type ProposedPayment, isPaymentModeWithFixedAmount, isProposedPaymentWithFixedAmount } from "./Payment"; import { PrimaryWithSecondaries } from "./PrimaryWithSecondaries"; import QRCode from "./QRCode"; +import { addToRecentlyUsed, getMostRecentlyUsed, removeFromRecentlyUsed } from "./recentlyUsed"; import { renderLogicalAssetAmount } from "./RenderLogicalAssetAmount"; +import { serializeCheckoutSettings, serializeCheckoutSettingsWithEncryption, serializeCheckoutSettingsWithSignature } from "./serialize"; import { Spinner } from "./Spinner"; -import { StrategyPreferences } from "./StrategyPreferences"; +import { type StrategyPreferences } from "./StrategyPreferences"; import { ToggleSwitch } from "./ToggleSwitch"; -import { allSupportedChainIds, getSupportedChainName } from "./chains"; -import { isLikelyAnEnsName } from "./isLikelyAnEnsName"; -import { isProduction } from "./isProduction"; -import { LogicalAssetTicker, parseLogicalAssetAmount } from "./logicalAssets"; -import { isTokenTickerSupportedByLogicalAsset } from "./logicalAssetsToTokens"; -import { addToRecentlyUsed, getMostRecentlyUsed, removeFromRecentlyUsed } from "./recentlyUsed"; -import { serializeCheckoutSettings, serializeCheckoutSettingsWithEncryption, serializeCheckoutSettingsWithSignature } from "./serialize"; -import { allTokenTickers } from "./tokens"; import { truncateEthAddress, truncateEthAddressVeryShort } from "./truncateAddress"; import { useAsyncMemo } from "./useAsyncMemo"; import useDebounce from "./useDebounce"; diff --git a/packages/react-app/src/TokenBalance.ts b/packages/react-app/src/TokenBalance.ts index bee036b7..6441b4ef 100644 --- a/packages/react-app/src/TokenBalance.ts +++ b/packages/react-app/src/TokenBalance.ts @@ -1,6 +1,5 @@ +import { getDecimalsToRenderForTokenTicker, getTokenByTokenKey, type TokenKey } from "@3cities/core"; import { formatUnits } from 'viem'; -import { getDecimalsToRenderForTokenTicker } from "./logicalAssetsToTokens"; -import { getTokenByTokenKey, TokenKey } from "./tokens"; // TokenBalance is a snapshot of an address's token balance on a // specific chain. For example, a snapshot of Bob's DAI balance on diff --git a/packages/react-app/src/blockExplorerUrls.ts b/packages/react-app/src/blockExplorerUrls.ts index 21a4fb5d..48a0345c 100644 --- a/packages/react-app/src/blockExplorerUrls.ts +++ b/packages/react-app/src/blockExplorerUrls.ts @@ -1,4 +1,4 @@ -import { getChain, type Chain } from "./chains"; +import { getChain, type Chain } from "@3cities/core"; export function getBlockExplorerUrlForTransaction(chainId: number | undefined, transactionHash: string): string | undefined { const chain: Chain | undefined = getChain(chainId); diff --git a/packages/react-app/src/canAfford.ts b/packages/react-app/src/canAfford.ts index d598dbf0..c824a5a0 100644 --- a/packages/react-app/src/canAfford.ts +++ b/packages/react-app/src/canAfford.ts @@ -1,5 +1,5 @@ -import { AddressContext } from "./AddressContext"; -import { TokenKey } from "./tokens"; +import { type TokenKey } from "@3cities/core"; +import { type AddressContext } from "./AddressContext"; // canAfford is a convenience predicate that returns true iff the passed // address context can afford to transfer the passed full-precision diff --git a/packages/react-app/src/crypto.ts b/packages/react-app/src/crypto.ts index 9dde969d..3b5142c7 100644 --- a/packages/react-app/src/crypto.ts +++ b/packages/react-app/src/crypto.ts @@ -1,4 +1,4 @@ -import { ElementType } from "./ElementType"; +import { type ElementType } from "./ElementType"; /* Notes on possible attack vectors: diff --git a/packages/react-app/images.d.ts b/packages/react-app/src/images.d.ts similarity index 100% rename from packages/react-app/images.d.ts rename to packages/react-app/src/images.d.ts diff --git a/packages/react-app/src/isProduction.ts b/packages/react-app/src/isProduction.ts deleted file mode 100644 index 744d06e4..00000000 --- a/packages/react-app/src/isProduction.ts +++ /dev/null @@ -1,5 +0,0 @@ - -// TODO split isProduction into two different dimensions with four vars, 1. isMainnetDependencies (isTestnetDependencies = !isMainnetDependencies) and 2. isProductionEnvironment (isDevelopmentEnvironment = !isProductionEnvironment) --> see .env-cmdrc.js --> then, update some usages of today's isProduction to instead be isMainnetDependencies. For example, token/chains should be mainnet based on isMainnetDependencies and not isProduction. For example, the "Open Link" dev feature in Request Money should use isDevelopmentEnvironment --> this change allows our three environments (dev, prod-test, prod) to properly configure themselves (eg. today prod-test has isProduction=false but instead it should have isMainnetDependencies=false isProduction=true) - -// isProduction is true if and only if the app is being run in production mode, ie. against production dependencies, eg. to use blockchain mainnets instead of testnets. Note that whether or not the app is being run in production mode (ie. the truthiness of isProduction) is orthogonal to the type of build being used to run the app. The app may be run with isProduction==true while in built in development mode (eg. to debug locally against production dependencies), or the app may be run with isProduction==false in a production minified build (eg. to offer a production deployment against test dependencies for demo purposes) -export const isProduction: boolean = process.env['REACT_APP_IS_PRODUCTION'] === 'true'; diff --git a/packages/react-app/src/serialize.ts b/packages/react-app/src/serialize.ts index c75f1271..2a66526c 100644 --- a/packages/react-app/src/serialize.ts +++ b/packages/react-app/src/serialize.ts @@ -1,14 +1,12 @@ -import { CheckoutSettings, SenderNoteSettings, SuccessActionRedirect } from "./CheckoutSettings"; -import { NonEmptyArray, ensureNonEmptyArray } from "./NonEmptyArray"; -import { PaymentMode, ProposedPayment, isPaymentModeWithFixedAmount } from "./Payment"; +import { type LogicalAssetTicker, type NonEmptyArray, allLogicalAssetTickers, ensureNonEmptyArray, hasOwnProperty, hasOwnPropertyOfType, toUppercase } from "@3cities/core"; +import { CheckoutSettingsEncrypted as CheckoutSettingsEncryptedPb, CheckoutSettings as CheckoutSettingsPb, CheckoutSettingsSigned as CheckoutSettingsSignedPb, MessageType as MessageTypePb, CheckoutSettings_PayWhatYouWant_PayWhatYouWantFlags as PayWhatYouWantFlagsPb, CheckoutSettings_PayWhatYouWant as PayWhatYouWantPb, CheckoutSettings_SenderNoteSettingsMode as SenderNoteSettingsModePb } from "@3cities/core/proto/checkout-settings"; +import { LogicalAssetTicker as LogicalAssetTickerPb } from "@3cities/core/proto/logical-assets"; +import { type CheckoutSettings, type SenderNoteSettings, type SuccessActionRedirect } from "./CheckoutSettings"; +import { type PaymentMode, type ProposedPayment, isPaymentModeWithFixedAmount } from "./Payment"; import { PrimaryWithSecondaries } from "./PrimaryWithSecondaries"; -import { StrategyPreferences } from "./StrategyPreferences"; +import { type StrategyPreferences } from "./StrategyPreferences"; import { modifiedBase64Decode, modifiedBase64Encode } from "./base64"; import { decrypt, encrypt, generateSignature, makeIv, makeSalt, verifySignature } from "./crypto"; -import { CheckoutSettingsEncrypted as CheckoutSettingsEncryptedPb, CheckoutSettings as CheckoutSettingsPb, CheckoutSettingsSigned as CheckoutSettingsSignedPb, LogicalAssetTicker as LogicalAssetTickerPb, MessageType as MessageTypePb, CheckoutSettings_PayWhatYouWant_PayWhatYouWantFlags as PayWhatYouWantFlagsPb, CheckoutSettings_PayWhatYouWant as PayWhatYouWantPb, CheckoutSettings_SenderNoteSettingsMode as SenderNoteSettingsModePb } from "./gen/threecities/v1/v1_pb"; -import { hasOwnProperty, hasOwnPropertyOfType } from "./hasOwnProperty"; -import { LogicalAssetTicker, allLogicalAssetTickers } from "./logicalAssets"; -import { toUppercase } from "./toUppercase"; // TODO unit tests for serialization functions. Especially a test that generates random CheckoutSettings and uses CheckoutSettingsPb.equals() to verify the serialization->deserialization didn't change anything diff --git a/packages/react-app/src/strategies.ts b/packages/react-app/src/strategies.ts index 75b97885..0512b4d3 100644 --- a/packages/react-app/src/strategies.ts +++ b/packages/react-app/src/strategies.ts @@ -1,18 +1,12 @@ -import { AddressContext } from "./AddressContext"; -import { ExchangeRates, convert } from "./ExchangeRates"; -import { Intersection } from "./Intersection"; -import { PaymentWithFixedAmount, ProposedPaymentWithFixedAmount, isPayment } from "./Payment"; -import { PrimaryWithSecondaries } from "./PrimaryWithSecondaries"; -import { StrategyPreferences } from "./StrategyPreferences"; -import { NativeCurrency, Token, isToken } from "./Token"; +import { type ExchangeRates, type LogicalAssetTicker, type NativeCurrency, type Token, allTokenTickers, arbitrum, arbitrumNova, base, baseSepolia, blast, chainsSupportedBy3cities, convert, convertLogicalAssetUnits, getAllNativeCurrenciesAndTokensForLogicalAssetTicker, getLogicalAssetTickerForTokenOrNativeCurrencyTicker, getTokenKey, immutableZkEvm, isProduction, isToken, isTokenSupported, linea, mainnet, mode, optimism, polygon, polygonZkEvm, scroll, sepolia, taiko, zkSync, zkSyncSepolia, zora } from "@3cities/core"; +import { type AddressContext } from "./AddressContext"; import { canAfford } from "./canAfford"; -import { arbitrum, arbitrumNova, base, baseSepolia, blast, chainsSupportedBy3cities, immutableZkEvm, linea, mainnet, mode, optimism, polygon, polygonZkEvm, scroll, sepolia, taiko, zkSync, zkSyncSepolia, zora } from './chains'; import { flatMap } from "./flatMap"; -import { isProduction } from "./isProduction"; -import { LogicalAssetTicker, convertLogicalAssetUnits } from "./logicalAssets"; -import { getAllNativeCurrenciesAndTokensForLogicalAssetTicker, getLogicalAssetTickerForTokenOrNativeCurrencyTicker } from "./logicalAssetsToTokens"; -import { ProposedTokenTransfer, TokenTransfer, TokenTransferForNativeCurrency, TokenTransferForToken } from "./tokenTransfer"; -import { allTokenTickers, getTokenKey, isTokenSupported } from "./tokens"; +import { type Intersection } from "./Intersection"; +import { type PaymentWithFixedAmount, type ProposedPaymentWithFixedAmount, isPayment } from "./Payment"; +import { PrimaryWithSecondaries } from "./PrimaryWithSecondaries"; +import { type StrategyPreferences } from "./StrategyPreferences"; +import { type ProposedTokenTransfer, type TokenTransfer, type TokenTransferForNativeCurrency, type TokenTransferForToken } from "./tokenTransfer"; // TODO consider s/Strategy.payment/Strategy.paymentWithFixedAmount` and same for ProposedStrategy --> on the other hand, the requirement that strategies operate on payments with fixed amounts represents unresolved tension between the concept of a Payment, Strategy, and the steps taken to settle a payment. In theory, a Payment of "can donate any asset from sender to receiver" should be able to be settled with some Strategy. But today, our strategy generation pipeline requires that token transfers be constructed from payments with fixed amounts. Today we have the concept "Payment with non-fixed amount must be pre-resovled by upstream into a payment with a fixed amount before it can have strategies generated" but in the future, we could have the concept "Payment with fixed or non-fixedd amount can have strategies generated, given sufficient context" diff --git a/packages/react-app/src/tokenTransfer.ts b/packages/react-app/src/tokenTransfer.ts index a519632d..fabb1014 100644 --- a/packages/react-app/src/tokenTransfer.ts +++ b/packages/react-app/src/tokenTransfer.ts @@ -1,7 +1,7 @@ -import { AddressOrEnsName } from "./AddressOrEnsName"; -import { Narrow } from "./Narrow"; -import { PartialFor } from "./PartialFor"; -import { NativeCurrency, Token, isToken } from "./Token"; +import { type NativeCurrency, type Token, isToken } from "@3cities/core"; +import { type AddressOrEnsName } from "./AddressOrEnsName"; +import { type Narrow } from "./Narrow"; +import { type PartialFor } from "./PartialFor"; // TokenTransfer represents a single token transfer of a native currency // or token from a sender to a receiver. The TokenTransferBase base type diff --git a/packages/react-app/src/transactions.tsx b/packages/react-app/src/transactions.tsx index 92c978a7..0e2fb567 100644 --- a/packages/react-app/src/transactions.tsx +++ b/packages/react-app/src/transactions.tsx @@ -1,17 +1,15 @@ +import { type Writable, getSupportedChainName, hasOwnPropertyOfType } from "@3cities/core"; import { ETHTransferProxyABI, getETHTransferProxyContractAddress } from "@3cities/eth-transfer-proxy"; import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { ChainMismatchError, SwitchChainError, UserRejectedRequestError, erc20Abi, type TransactionReceipt } from "viem"; +import { ChainMismatchError, SwitchChainError, type TransactionReceipt, UserRejectedRequestError, erc20Abi } from "viem"; import { serialize, useAccount, useEstimateGas, useSendTransaction, useSimulateContract, useSwitchChain, useWaitForTransactionReceipt, useWriteContract } from 'wagmi'; -import { SwitchChainErrorType } from "wagmi/actions"; -import { Intersection } from "./Intersection"; -import { Narrow } from "./Narrow"; -import { PartialFor } from "./PartialFor"; +import { type SwitchChainErrorType } from "wagmi/actions"; +import { type Intersection } from "./Intersection"; +import { type Narrow } from "./Narrow"; +import { type Observer, makeObservableValue, useObservedValue } from "./observer"; +import { type PartialFor } from "./PartialFor"; import { Spinner } from "./Spinner"; -import { Writable } from "./Writable"; -import { getSupportedChainName } from "./chains"; -import { hasOwnPropertyOfType } from "./hasOwnProperty"; -import { Observer, makeObservableValue, useObservedValue } from "./observer"; -import { TokenTransfer, TokenTransferForNativeCurrency, TokenTransferForToken, isTokenAndNotNativeCurrencyTransfer } from "./tokenTransfer"; +import { type TokenTransfer, type TokenTransferForNativeCurrency, type TokenTransferForToken, isTokenAndNotNativeCurrencyTransfer } from "./tokenTransfer"; import { useMemoObject } from "./useMemoObject"; // TODO build and save list of test cases to check all ExecuteTokenTransfer code paths, eg. (automatic retries, other features) X (token, native currency) X (wallets) X (chains) X (different transfer amounts including very small amounts) diff --git a/packages/react-app/src/useActiveDemoAccount.ts b/packages/react-app/src/useActiveDemoAccount.ts index 16807293..a4c2586a 100644 --- a/packages/react-app/src/useActiveDemoAccount.ts +++ b/packages/react-app/src/useActiveDemoAccount.ts @@ -1,6 +1,6 @@ import { useContext } from "react"; import { ActiveDemoAccountContext } from "./ActiveDemoAccountContext"; -import { Observer, useObservedValue } from "./observer"; +import { type Observer, useObservedValue } from "./observer"; // useActiveDemoAccount returns the active global demo account or // undefined if no demo account is active. diff --git a/packages/react-app/src/useAsyncMemo.ts b/packages/react-app/src/useAsyncMemo.ts index 76f5e032..3fba9635 100644 --- a/packages/react-app/src/useAsyncMemo.ts +++ b/packages/react-app/src/useAsyncMemo.ts @@ -1,4 +1,4 @@ -import { DependencyList, useEffect, useMemo, useState } from 'react'; +import { type DependencyList, useEffect, useMemo, useState } from 'react'; // Design goals of useAsyncMemo (which were achieved) // 1. mimic React.useMemo's API but for async values. diff --git a/packages/react-app/src/useBestStrategy.ts b/packages/react-app/src/useBestStrategy.ts index 4884705c..5d76dfe2 100644 --- a/packages/react-app/src/useBestStrategy.ts +++ b/packages/react-app/src/useBestStrategy.ts @@ -1,7 +1,7 @@ +import { getTokenKey } from "@3cities/core"; import { useCallback, useMemo, useState } from "react"; import { useImmer } from "use-immer"; -import { Strategy } from "./strategies"; -import { getTokenKey } from "./tokens"; +import { type Strategy } from "./strategies"; const emptySet = new Set(); // WARNING useImmer may use object identity for render stability and so we pass a static object as the default value to avoid rerenders diff --git a/packages/react-app/src/useCaip222StyleSignature.ts b/packages/react-app/src/useCaip222StyleSignature.ts index 299f7357..f0c5a033 100644 --- a/packages/react-app/src/useCaip222StyleSignature.ts +++ b/packages/react-app/src/useCaip222StyleSignature.ts @@ -1,55 +1,15 @@ +import { type Caip222StyleMessageToSign, type Caip222StyleSignature, caip222StyleSignatureMessageDomain, caip222StyleSignatureMessagePrimaryType, caip222StyleSignatureMessageTypes, chainIdOnWhichToSignMessagesAndVerifySignatures, erc1271MagicValue, erc1271SmartAccountAbi, hasOwnPropertyOfType } from "@3cities/core"; import { useCallback, useEffect, useMemo, useState } from "react"; import { UserRejectedRequestError, hashTypedData } from "viem"; import { useAccount, useReadContract, useSignTypedData, useSwitchChain, useVerifyTypedData } from 'wagmi'; -import { mainnet, sepolia } from "./chains"; -import { hasOwnPropertyOfType } from "./hasOwnProperty"; -import { isProduction } from "./isProduction"; import { useLiveReloadQueryOptions } from "./useLiveReloadQueryOptions"; -// ERC-1271 -// isValidSignature(bytes32 hash, bytes signature) → bytes4 magicValue -export const smartAccountAbi = [ - { - name: 'isValidSignature', - type: 'function', - stateMutability: 'view', - inputs: [ - { name: 'hash', type: 'bytes32' }, - { name: 'signature', type: 'bytes' }, - ], - outputs: [{ name: '', type: 'bytes4' }], - }, -] as const; - -export const eip1271MagicValue = "0x1626ba7e" as const; - -export const domain = { - name: '3cities', - version: '1', - // NB chainId is not needed for our message as we only want to prove sender address ownership (on any chain, assuming it then applies to all chains). Note that any chainId provided here is purely signed data and not actually used by any wallet to eg. switch to that chainId prior to signing -} as const; - -export const types = { - SenderAddress: [ - { name: 'senderAddress', type: 'address' }, - ], -} as const; - -export const primaryType = 'SenderAddress' as const; - -export type Caip222StyleSignature = `0x${string}` | `eip1271-chainId-${number}`; // a successfully collected Caip222-style signature. `0x${string}` indicates an ordinary signature. `eip1271-chainId-${number}` indicates a smart contract wallet verified the message using eip1271 verification via a isValidSignature call on the provided chainId - -export type Caip222StyleMessageToSign = { - senderAddress: `0x${string}`; -}; type UseCaip222StyleSignatureParams = { enabled?: boolean; // iff true, the subsystem will attempt to collect a signature. If undefined or false, the subsystem will be disabled and will always return idle eip1271ChainId: number | undefined; // chain on which eip1271 isValidSignature verification will be attempted. Smart contract wallets implementing eip1271, like Gnosis Safe, don't actually produce signatures for signed messages. Instead, they keep an onchain collection of verified hashes of "signed" messages. This chain ID must be provided, or signature generation will never be attempted and the result will never become available, because this library currently doesn't know if the connected address uses ordinary signature generation/verification or eip1271-based signature verification. } -export const caip222ChainId: number = isProduction ? mainnet.id : sepolia.id; // in certain caip222Style contexts, a concrete chainId is needed. When a chainId is needed, this one must be used. - // useCaip222StyleSignature is an API to ask the user to sign a // CAIP-222-style message to verify ownership of the connected address. // It's compatible if the connected wallet address is an EOA (eg. @@ -138,14 +98,14 @@ export function useCaip222StyleSignature({ enabled: useCaip222StyleSignatureEnab const queryOpts = useLiveReloadQueryOptions(); const { error: eip1271ContractReadError, isSuccess: eip1271ContractReadIsSuccess, data: eip1271RawResult } = useReadContract(messageToSign && eip1271ChainId ? { - abi: smartAccountAbi, + abi: erc1271SmartAccountAbi, chainId: eip1271ChainId, address: connectedAddress, functionName: 'isValidSignature', args: [hashTypedData({ - domain, - types, - primaryType, + domain: caip222StyleSignatureMessageDomain, + types: caip222StyleSignatureMessageTypes, + primaryType: caip222StyleSignatureMessagePrimaryType, message: messageToSign, }), '0x'], query: { @@ -156,7 +116,7 @@ export function useCaip222StyleSignature({ enabled: useCaip222StyleSignatureEnab }, } : undefined); - const eip1271VerificationSuccessful = eip1271ContractReadIsSuccess && eip1271RawResult === eip1271MagicValue; + const eip1271VerificationSuccessful = eip1271ContractReadIsSuccess && eip1271RawResult === erc1271MagicValue; useEffect(() => { if (eip1271ContractReadError && !signTypedDataEnabled) setSignTypedDataEnabled(true); @@ -177,11 +137,11 @@ export function useCaip222StyleSignature({ enabled: useCaip222StyleSignatureEnab const sign = useMemo((): (() => Promise) | undefined => signTypedDataEnabled && messageToSign ? async () => { setSignCalledAtLeastOnce(true); try { - if (doesSignatureDependOnActiveChain && (activeChain === undefined || activeChain.id !== caip222ChainId)) await switchChainAsync({ chainId: caip222ChainId }); // NB the reason we bother computing `doesSignatureDependOnActiveChain` and don't just switchChain for all wallets is because for some wallets, that would result in potentially multiple switch chain pop-ups per checkout. Eg. with MetaMask, one switch chain prior to the signature, and then another switch chain for the chosen payment method - that'd be terrible UX. + if (doesSignatureDependOnActiveChain && (activeChain === undefined || activeChain.id !== chainIdOnWhichToSignMessagesAndVerifySignatures)) await switchChainAsync({ chainId: chainIdOnWhichToSignMessagesAndVerifySignatures }); // NB the reason we bother computing `doesSignatureDependOnActiveChain` and don't just switchChain for all wallets is because for some wallets, that would result in potentially multiple switch chain pop-ups per checkout. Eg. with MetaMask, one switch chain prior to the signature, and then another switch chain for the chosen payment method - that'd be terrible UX. signTypedData({ - domain, - types, - primaryType, + domain: caip222StyleSignatureMessageDomain, + types: caip222StyleSignatureMessageTypes, + primaryType: caip222StyleSignatureMessagePrimaryType, message: messageToSign, }); } catch (e) { @@ -190,12 +150,11 @@ export function useCaip222StyleSignature({ enabled: useCaip222StyleSignatureEnab } : undefined, [activeChain, messageToSign, signTypedDataEnabled, doesSignatureDependOnActiveChain, signTypedData, switchChainAsync]); const { data: useSignTypedDataIsVerified, isLoading: useVerifyTypedDataIsLoading, error: useVerifyTypedDataError } = useVerifyTypedData({ - // WARNING passing `chainId` here as some wallets (like coinbase smart wallet) will sign the message in the context of whatever chain they currently have selected, and then verification will fail if the passed chainId doesn't match this currently selected chain. Instead, we don't pass chainId and the wallet will sign and verify the message in the context of the same chain - chainId: caip222ChainId, // NB see the discussion above about "three kinds of wallets". For (1) EOA wallets, it doesn't matter on which chainId the signature is verified, but we pass it to ensure the chainId on which verification occurs is supported by our wagmiConfig. For (2) eip1271 smart contract wallets, useVerifyTypedData is unused because useSignTypedData hangs in pending forever. For (3) counterfactually instantiated smart contract wallets, the chainId used for verification is highly meaningful because the signatures generated are conditional on the active chain at the time of generation, and so WARNING this chainId must match the chainId on which the counterfactual smart wallet signature was generated. + chainId: chainIdOnWhichToSignMessagesAndVerifySignatures, // NB see the discussion above about "three kinds of wallets". For (1) EOA wallets, it doesn't matter on which chainId the signature is verified, but we pass it to ensure the chainId on which verification occurs is supported by our wagmiConfig. For (2) eip1271 smart contract wallets, useVerifyTypedData is unused because useSignTypedData might hang in pending forever. (WARNING: useSignTypedData hangs forever for our gnosis safe with walletconnect, but this is not intended behavior, https://github.com/WalletConnect/walletconnect-monorepo/issues/4464#issuecomment-2219668086.) For (3) counterfactually instantiated smart contract wallets, the chainId used for verification is highly meaningful because the signatures generated are conditional on the active chain at the time of generation, and so WARNING this chainId must match the chainId on which the counterfactual smart wallet signature was generated. address: connectedAddress, - domain, - types, - primaryType, + domain: caip222StyleSignatureMessageDomain, + types: caip222StyleSignatureMessageTypes, + primaryType: caip222StyleSignatureMessagePrimaryType, message: messageToSign, signature: rawSignature, }); diff --git a/packages/react-app/src/useCheckoutSettings.ts b/packages/react-app/src/useCheckoutSettings.ts index 376da683..391ef184 100644 --- a/packages/react-app/src/useCheckoutSettings.ts +++ b/packages/react-app/src/useCheckoutSettings.ts @@ -1,11 +1,11 @@ +import { type LogicalAssetTicker, allLogicalAssetTickers, parseLogicalAssetAmount } from "@3cities/core"; import { useContext, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import { isHex, parseUnits } from "viem"; -import { AuthenticateSenderAddress, CheckoutSettings } from "./CheckoutSettings"; -import { CheckoutSettingsContext, CheckoutSettingsRequiresPassword, isCheckoutSettingsRequiresPassword } from "./CheckoutSettingsContext"; -import { ProposedPayment, isProposedPaymentWithFixedAmount } from "./Payment"; +import { type AuthenticateSenderAddress, type CheckoutSettings } from "./CheckoutSettings"; +import { CheckoutSettingsContext, type CheckoutSettingsRequiresPassword, isCheckoutSettingsRequiresPassword } from "./CheckoutSettingsContext"; +import { type ProposedPayment, isProposedPaymentWithFixedAmount } from "./Payment"; import { PrimaryWithSecondaries } from "./PrimaryWithSecondaries"; -import { LogicalAssetTicker, allLogicalAssetTickers, parseLogicalAssetAmount } from "./logicalAssets"; // useCheckoutSettings returns the contextual CheckoutSettings that's // been provided by CheckoutSettingsProvider, or a diff --git a/packages/react-app/src/useConnectedAccountContext.ts b/packages/react-app/src/useConnectedAccountContext.ts index 51cdc608..9eafdad0 100644 --- a/packages/react-app/src/useConnectedAccountContext.ts +++ b/packages/react-app/src/useConnectedAccountContext.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; -import { AddressContext } from './AddressContext'; +import { type AddressContext } from './AddressContext'; import { ConnectedAccountContextObserverContext } from './ConnectedAccountContextObserverContext'; -import { Observer, useObservedValue } from './observer'; +import { type Observer, useObservedValue } from './observer'; // useConnectedAccountContext returns the connected account's // AddressContext, which is automatically kept synced with latest diff --git a/packages/react-app/src/useEffectSkipFirst.ts b/packages/react-app/src/useEffectSkipFirst.ts index 92e5e936..f6019eb5 100644 --- a/packages/react-app/src/useEffectSkipFirst.ts +++ b/packages/react-app/src/useEffectSkipFirst.ts @@ -1,4 +1,4 @@ -import { DependencyList, EffectCallback, useEffect, useRef } from "react"; +import { type DependencyList, type EffectCallback, useEffect, useRef } from "react"; // useEffectSkipFirst is a wrapper around useEffect that skips the first // invocation of the useEffect callback. This is useful to eg. run an diff --git a/packages/react-app/src/useEnsAddress.ts b/packages/react-app/src/useEnsAddress.ts index 41f517b4..20dae3d7 100644 --- a/packages/react-app/src/useEnsAddress.ts +++ b/packages/react-app/src/useEnsAddress.ts @@ -1,8 +1,7 @@ +import { isProduction, mainnet, sepolia } from "@3cities/core"; import { useMemo } from "react"; import { isAddress } from "viem"; import { useEnsAddress as wagmiUseEnsAddress } from 'wagmi'; -import { mainnet, sepolia } from "./chains"; -import { isProduction } from "./isProduction"; import { useLiveReloadQueryOptions } from "./useLiveReloadQueryOptions"; // useEnsAddress is our higher-level wrapper around wagmi.useEnsAddress. diff --git a/packages/react-app/src/useEnsName.ts b/packages/react-app/src/useEnsName.ts index 37a4ea37..975dd5cd 100644 --- a/packages/react-app/src/useEnsName.ts +++ b/packages/react-app/src/useEnsName.ts @@ -1,8 +1,7 @@ +import { isProduction, mainnet, sepolia } from "@3cities/core"; import { useMemo } from "react"; import { isAddress } from "viem"; import { useEnsName as wagmiUseEnsName } from 'wagmi'; -import { mainnet, sepolia } from "./chains"; -import { isProduction } from "./isProduction"; import { truncateEnsName } from "./truncateAddress"; import { useEnsAddress } from "./useEnsAddress"; import { useLiveReloadQueryOptions } from "./useLiveReloadQueryOptions"; diff --git a/packages/react-app/src/useExchangeRates.ts b/packages/react-app/src/useExchangeRates.ts index 1046512e..a2070d05 100644 --- a/packages/react-app/src/useExchangeRates.ts +++ b/packages/react-app/src/useExchangeRates.ts @@ -1,7 +1,7 @@ +import { type ExchangeRates, mergeExchangeRates } from "@3cities/core"; import { useContext, useMemo } from "react"; -import { ExchangeRates, mergeExchangeRates } from "./ExchangeRates"; import { ExchangeRatesContext } from "./ExchangeRatesContext"; -import { Observer, useObservedValue } from "./observer"; +import { type Observer, useObservedValue } from "./observer"; // useExchangeRates returns the global exchange rates, which are // automatically kept up-to-date with the latest exchange rates for diff --git a/packages/react-app/src/useInput.tsx b/packages/react-app/src/useInput.tsx index 594634b2..11501a75 100644 --- a/packages/react-app/src/useInput.tsx +++ b/packages/react-app/src/useInput.tsx @@ -1,4 +1,4 @@ -import React, { InputHTMLAttributes, useCallback, useMemo, useState } from 'react'; +import React, { type InputHTMLAttributes, useCallback, useMemo, useState } from 'react'; interface Opts { onEnterKeyPress?: (e: React.KeyboardEvent) => void; // callback that will be invoked when the user hits the enter key. diff --git a/packages/react-app/src/useIsPageVisibleOrRecentlyVisible.tsx b/packages/react-app/src/useIsPageVisibleOrRecentlyVisible.tsx index 06de1e09..f04fae84 100644 --- a/packages/react-app/src/useIsPageVisibleOrRecentlyVisible.tsx +++ b/packages/react-app/src/useIsPageVisibleOrRecentlyVisible.tsx @@ -1,6 +1,6 @@ import { useContext } from "react"; import { IsPageVisibleOrRecentlyVisibleContext } from "./IsPageVisibleOrRecentlyVisibleContext"; -import { Observer, useObservedValue } from "./observer"; +import { type Observer, useObservedValue } from "./observer"; // useIsPageVisibleOrRecentlyVisible returns true iff the page is // currently visible or has been visible in the past opts.recentMillis diff --git a/packages/react-app/src/useLogicalAssetTickerSelectionInput.tsx b/packages/react-app/src/useLogicalAssetTickerSelectionInput.tsx index d7a05921..31cbe53e 100644 --- a/packages/react-app/src/useLogicalAssetTickerSelectionInput.tsx +++ b/packages/react-app/src/useLogicalAssetTickerSelectionInput.tsx @@ -1,5 +1,5 @@ +import { type LogicalAssetTicker, logicalAssetsByTicker } from "@3cities/core"; import React, { useMemo, useState } from "react"; -import { LogicalAssetTicker, logicalAssetsByTicker } from "./logicalAssets"; // TODO consider converting useLogicalAssetTickerSelectionInput into the // LogicalAssetTickerSelectionInput component so that clients aren't diff --git a/packages/react-app/src/usePayWhatYouWantInput.tsx b/packages/react-app/src/usePayWhatYouWantInput.tsx index 7e3db148..c458e7d4 100644 --- a/packages/react-app/src/usePayWhatYouWantInput.tsx +++ b/packages/react-app/src/usePayWhatYouWantInput.tsx @@ -1,8 +1,8 @@ +import { parseLogicalAssetAmount } from "@3cities/core"; import React, { useMemo, useState } from "react"; -import { PayWhatYouWant } from "./Payment"; +import { type PayWhatYouWant } from "./Payment"; import { ToggleSwitch } from "./ToggleSwitch"; import { flatMap } from "./flatMap"; -import { parseLogicalAssetAmount } from "./logicalAssets"; import { useInput } from "./useInput"; // TODO consider converting usePayWhatYouWantInput into the diff --git a/packages/react-app/src/useProposedPaymentReceiverAddressAndEnsName.ts b/packages/react-app/src/useProposedPaymentReceiverAddressAndEnsName.ts index 2cdd8929..dd4a5ea2 100644 --- a/packages/react-app/src/useProposedPaymentReceiverAddressAndEnsName.ts +++ b/packages/react-app/src/useProposedPaymentReceiverAddressAndEnsName.ts @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { ProposedPayment, isProposedPaymentWithReceiverAddress } from "./Payment"; +import { type ProposedPayment, isProposedPaymentWithReceiverAddress } from "./Payment"; import { useEnsAddress } from "./useEnsAddress"; import { useEnsName } from "./useEnsName"; diff --git a/packages/react-app/src/wagmiClient.ts b/packages/react-app/src/wagmiClient.ts index d43ff552..361e3873 100644 --- a/packages/react-app/src/wagmiClient.ts +++ b/packages/react-app/src/wagmiClient.ts @@ -1,9 +1,8 @@ // TODO s/wagmiClient.ts/wagmiConfig.ts/ +import { alchemyHttpUrl, chainsSupportedBy3cities, infuraHttpUrl } from '@3cities/core'; import { createConfig, fallback, http, type Transport } from '@wagmi/core'; import { coinbaseWallet, injected, walletConnect } from 'wagmi/connectors'; -import { chainsSupportedBy3cities } from './chains'; -import { alchemyHttpUrl, infuraHttpUrl } from './rpcUrls'; const walletConnectProjectId: string = (() => { const s = process.env['REACT_APP_WALLETCONNECT_PROJECT_ID']; diff --git a/packages/react-app/tsconfig.json b/packages/react-app/tsconfig.json index b1ed1da4..62b54be0 100644 --- a/packages/react-app/tsconfig.json +++ b/packages/react-app/tsconfig.json @@ -25,9 +25,9 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ + "module": "es2020", /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ diff --git a/packages/verifier/.eslintrc.js b/packages/verifier/.eslintrc.js new file mode 100644 index 00000000..e40864c8 --- /dev/null +++ b/packages/verifier/.eslintrc.js @@ -0,0 +1,11 @@ +// This package-specific .eslintrc.js file is automatically merged by `eslint` at runtime with our project's root .eslintrs.js https://eslint.org/docs/latest/use/configure/configuration-files#cascading-and-hierarchy +const path = require('path'); + +module.exports = { + "env": { + "es2023": true, + }, + "parserOptions": { + "project": path.resolve(__dirname, './tsconfig.json'), + }, +}; diff --git a/packages/verifier/.gitignore b/packages/verifier/.gitignore new file mode 100644 index 00000000..629ac705 --- /dev/null +++ b/packages/verifier/.gitignore @@ -0,0 +1,2 @@ +# build directories +dist diff --git a/packages/verifier/README.md b/packages/verifier/README.md new file mode 100644 index 00000000..9389a594 --- /dev/null +++ b/packages/verifier/README.md @@ -0,0 +1,6 @@ + +# Overview + +TODO + +# Usage diff --git a/packages/verifier/package.json b/packages/verifier/package.json new file mode 100644 index 00000000..a8761368 --- /dev/null +++ b/packages/verifier/package.json @@ -0,0 +1,45 @@ +{ + "name": "@3cities/verifier", + "license": "MIT", + "version": "1.0.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist/cjs/**/*", + "dist/esm/**/*", + "dist/types/**/*" + ], + "exports": { + ".": { + "require": "./dist/cjs/index.js", + "import": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts" + } + }, + "dependencies": { + "@3cities/core": "^1.0.0" + }, + "devDependencies": { + "nodemon": "^3.1.4" + }, + "scripts": { + "clean:ts": "rm -rf ./dist", + "clean:cjs": "rm -rf ./dist/cjs", + "clean:esm": "rm -rf ./dist/esm", + "clean:types": "rm -rf ./dist/types", + "clean": "yarn clean:ts", + "build:cjs": "yarn clean:cjs && tsc --module commonjs --moduleResolution node --outDir ./dist/cjs --verbatimModuleSyntax false --sourceMap", + "build:esm": "yarn clean:esm && tsc --outDir ./dist/esm --sourceMap", + "build:types": "yarn clean:types && tsc --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", + "build:ts": "yarn build:cjs && yarn build:esm && yarn build:types", + "build": "yarn build:ts", + "build:dev": "yarn build # NB verifier has no dev build, but we include this so that the root build:dev task works for all packages. Clients must apply any env-specific settings", + "build:prod-test": "yarn build # NB verifier has no prod-test build, but we include this so that the root build:prod-test task works for all packages. Clients must apply any env-specific settings", + "lint": "eslint . --max-warnings 0", + "prepack": "yarn && yarn build", + "start": "nodemon --watch 'src/**/*' --ext 'ts,proto' --exec 'yarn build'", + "test": "forge test", + "test:coverage": "forge coverage" + } +} \ No newline at end of file diff --git a/packages/verifier/src/index.ts b/packages/verifier/src/index.ts new file mode 100644 index 00000000..db10a16d --- /dev/null +++ b/packages/verifier/src/index.ts @@ -0,0 +1,3 @@ +import { type TransferVerificationRequest, verifyTransfer } from "./verifyTransfer"; + +export { verifyTransfer, type TransferVerificationRequest }; diff --git a/packages/react-app/src/verifierPrototype.ts b/packages/verifier/src/verifyTransfer.ts similarity index 93% rename from packages/react-app/src/verifierPrototype.ts rename to packages/verifier/src/verifyTransfer.ts index 3e45f6d5..ca9903d6 100644 --- a/packages/react-app/src/verifierPrototype.ts +++ b/packages/verifier/src/verifyTransfer.ts @@ -1,15 +1,8 @@ +import { caip222StyleSignatureMessageDomain, caip222StyleSignatureMessagePrimaryType, caip222StyleSignatureMessageTypes, chainIdOnWhichToSignMessagesAndVerifySignatures, chainsSupportedBy3cities, convert, convertLogicalAssetUnits, erc1271MagicValue, erc1271SmartAccountAbi, getConfirmationsToWait, getLogicalAssetTickerForTokenOrNativeCurrencyTicker, nativeCurrencies, tokens, type Caip222StyleMessageToSign, type Caip222StyleSignature } from "@3cities/core"; import { ETHTransferProxyABI, getETHTransferProxyContractAddress } from "@3cities/eth-transfer-proxy"; import { getTransactionConfirmations, getTransactionReceipt, readContract, verifyTypedData, type Config } from "@wagmi/core"; import { erc20Abi, hashTypedData, isHex, parseEventLogs } from "viem"; import { serialize } from "wagmi"; -import { convert } from "./ExchangeRates"; -import { mainnet, sepolia, type Chain } from "./chains"; -import { getConfirmationsToWait } from "./getConfirmationsToWait"; -import { isProduction } from "./isProduction"; -import { convertLogicalAssetUnits } from "./logicalAssets"; -import { getLogicalAssetTickerForTokenOrNativeCurrencyTicker } from "./logicalAssetsToTokens"; -import { nativeCurrencies, tokens } from "./tokens"; -import { Caip222StyleMessageToSign, Caip222StyleSignature, domain, eip1271MagicValue, primaryType, smartAccountAbi, types } from "./useCaip222StyleSignature"; export type TransferVerificationRequest = { trusted: { // from the point of view of the verification client (caller), these data are trusted and will be used to verify the untrustedToBeVerified data @@ -51,12 +44,15 @@ type TransferVerificationResult = { } | { successVerified: false; description: string; - error?: Error; // TODO should Error be unconditionally defined when successVerified=false? probably? + error?: Error; // TODO should Error be unconditionally defined when successVerified=false? probably? failureReason: "todo"; successData?: never; }); -export async function verify(wagmiConfig: Config, supportedChains: Chain[], req: TransferVerificationRequest): Promise { +export async function verifyTransfer({ wagmiConfig, req }: { + wagmiConfig: Config, + req: TransferVerificationRequest, +}): Promise { const [senderAddress, senderAddressError]: [`0x${string}`, undefined] | [undefined, Error] = (() => { const a = req.untrustedToBeVerified.senderAddress; const b = req.untrustedToBeVerified.caip222StyleSignature?.message.senderAddress; @@ -75,7 +71,7 @@ export async function verify(wagmiConfig: Config, supportedChains: Chain[], req: description: `Invalid request: sender address error`, error: senderAddressError, failureReason: "todo", - }; else if (supportedChains.find(c => c.id === req.untrustedToBeVerified.chainId) === undefined) return { + }; else if (chainsSupportedBy3cities.find(c => c.id === req.untrustedToBeVerified.chainId) === undefined) return { successVerified: false, description: `Chain ID ${req.untrustedToBeVerified.chainId} is unsupported by 3cities`, failureReason: "todo", // TODO eg. CHAIN_ID_UNSUPPORTED @@ -87,25 +83,25 @@ export async function verify(wagmiConfig: Config, supportedChains: Chain[], req: const getTransactionReceiptPromise = getTransactionReceipt(wagmiConfig, { hash: req.untrustedToBeVerified.transactionHash, chainId: req.untrustedToBeVerified.chainId }); const getTransactionConfirmationsPromise = getTransactionConfirmations(wagmiConfig, { hash: req.untrustedToBeVerified.transactionHash, chainId: req.untrustedToBeVerified.chainId }); const verifyTypedDataPromise: Promise | undefined = req.untrustedToBeVerified.caip222StyleSignature && isHex(req.untrustedToBeVerified.caip222StyleSignature.signature) ? verifyTypedData(wagmiConfig, { - chainId: isProduction ? mainnet.id : sepolia.id, // TODO pass this is in as verifier does not have production/non-production builds + chainId: chainIdOnWhichToSignMessagesAndVerifySignatures, address: req.untrustedToBeVerified.caip222StyleSignature.message.senderAddress, - domain, - types, - primaryType, + domain: caip222StyleSignatureMessageDomain, + types: caip222StyleSignatureMessageTypes, + primaryType: caip222StyleSignatureMessagePrimaryType, message: req.untrustedToBeVerified.caip222StyleSignature.message, signature: req.untrustedToBeVerified.caip222StyleSignature.signature, }) : undefined; const eip1271ChainId: number | undefined = req.untrustedToBeVerified.caip222StyleSignature && !isHex(req.untrustedToBeVerified.caip222StyleSignature.signature) ? parseInt(req.untrustedToBeVerified.caip222StyleSignature.signature.split('eip1271-chainId-')[1] || '') : undefined; const eip1271IsValidSignaturePromise = req.untrustedToBeVerified.caip222StyleSignature && eip1271ChainId && !isNaN(eip1271ChainId) ? readContract(wagmiConfig, { // NB here we know that caip222StyleSignature is defined if eip1271ChainId is defined, but the typescript compiler doesn't - abi: smartAccountAbi, + abi: erc1271SmartAccountAbi, chainId: eip1271ChainId, address: req.untrustedToBeVerified.caip222StyleSignature.message.senderAddress, functionName: 'isValidSignature', args: [hashTypedData({ - domain, - types, - primaryType, + domain: caip222StyleSignatureMessageDomain, + types: caip222StyleSignatureMessageTypes, + primaryType: caip222StyleSignatureMessagePrimaryType, message: req.untrustedToBeVerified.caip222StyleSignature.message, }), '0x'], }) : undefined; @@ -139,7 +135,7 @@ export async function verify(wagmiConfig: Config, supportedChains: Chain[], req: const [eip1271SignatureIsVerified, eip1271SignatureVerificationError]: [Awaited> | undefined, undefined] | [undefined, Error] = await (async () => { try { const v = await eip1271IsValidSignaturePromise; - return [v === undefined ? undefined : v === eip1271MagicValue, undefined]; + return [v === undefined ? undefined : v === erc1271MagicValue, undefined]; } catch (e) { return [undefined, Error(`Failed to verify caip222-style eip1271 signature. tx hash=${req.untrustedToBeVerified.transactionHash} chainId=${req.untrustedToBeVerified.chainId} caip222=${JSON.stringify(req.untrustedToBeVerified.caip222StyleSignature)} eip1271ChainId=${eip1271ChainId}`, { cause: e })]; } @@ -159,7 +155,7 @@ export async function verify(wagmiConfig: Config, supportedChains: Chain[], req: successVerified: false, description: `Transaction reverted`, failureReason: "todo", // TODO eg. TRANSACTION_REVERTED - }; else if (txConfirmations < 0 /* TODO actual: confirmationsToWait */) return { + }; else if (txConfirmations < confirmationsToWait) return { successVerified: false, description: `Transaction has insufficient confirmations, wanted=${confirmationsToWait}, found=${txConfirmations}`, failureReason: "todo", // TODO eg. TRANSACTION_HAS_INSUFFICIENT_CONFIRMATIONS @@ -190,7 +186,7 @@ export async function verify(wagmiConfig: Config, supportedChains: Chain[], req: logs: tx.logs, }).filter(l => l.address.toLowerCase() === ethTransferProxyContractAddress.toLowerCase()) : []; - const tokensAllowedOnThisChain = tokens.filter(t => t.chainId === req.untrustedToBeVerified.chainId).filter(t => req.trusted.tokenTickerAllowlist.map(tt => tt.toLowerCase()).includes(t.ticker.toLowerCase())); // TODO pass in tokens as verifier doesn't have production/non-production builds + const tokensAllowedOnThisChain = tokens.filter(t => t.chainId === req.untrustedToBeVerified.chainId).filter(t => req.trusted.tokenTickerAllowlist.map(tt => tt.toLowerCase()).includes(t.ticker.toLowerCase())); const erc20TransferLogs = parseEventLogs({ abi: erc20Abi, diff --git a/packages/verifier/tsconfig.json b/packages/verifier/tsconfig.json new file mode 100644 index 00000000..4429626e --- /dev/null +++ b/packages/verifier/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src/**/*.ts", + ], +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 7cd1b6a1..9d37cc35 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -89,7 +89,7 @@ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - // "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": [], } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e6124c50..f569fbc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5097,7 +5097,7 @@ check-types@^11.2.3: resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.2.3.tgz#1ffdf68faae4e941fce252840b1787b8edc93b71" integrity sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg== -chokidar@^3.4.2, chokidar@^3.5.3, chokidar@^3.6.0: +chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -5840,7 +5840,7 @@ debug@2.6.9, debug@^2.6.0: dependencies: ms "2.0.0" -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== @@ -8048,6 +8048,11 @@ ieee754@^1.1.13, ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" @@ -9941,6 +9946,22 @@ node-stdlib-browser@^1.2.0: util "^0.12.4" vm-browserify "^1.0.1" +nodemon@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.4.tgz#c34dcd8eb46a05723ccde60cbdd25addcc8725e4" + integrity sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -11258,6 +11279,11 @@ psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + public-encrypt@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" @@ -12279,6 +12305,13 @@ signal-exit@^4.1.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -13037,6 +13070,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + tough-cookie@^4.0.0: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" @@ -13292,6 +13330,11 @@ uncrypto@^0.1.3: resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + underscore@1.12.1: version "1.12.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e"