diff --git a/package-lock.json b/package-lock.json index a9048406..1f68e4ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@hazae41/cascade": "^1.1.27", "@hazae41/chacha20poly1305": "^1.0.2", "@hazae41/cleaner": "^2.0.3", - "@hazae41/cubane": "^0.0.11", + "@hazae41/cubane": "^0.0.12", "@hazae41/cursor": "^1.1.26", "@hazae41/echalote": "^0.2.80", "@hazae41/ed25519": "^2.1.6", @@ -60,7 +60,7 @@ "@types/chrome": "^0.0.246", "@types/node": "20.8.2", "@types/react": "18.2.24", - "@types/react-dom": "18.2.8", + "@types/react-dom": "18.2.10", "@types/w3c-web-usb": "^1.0.7", "autoprefixer": "^10.4.16", "eslint": "8.50.0", @@ -2180,16 +2180,18 @@ "integrity": "sha512-dygrCKDPb4LaWcRRceb32z0sVE83+RHq8eCyHYNLSvrASjTzalJBMfsqZQayBqKcuG7tV9ymTIDf1gvF8dtM2Q==" }, "node_modules/@hazae41/cubane": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@hazae41/cubane/-/cubane-0.0.11.tgz", - "integrity": "sha512-xoArdcbcl5ahg7HgdfkivSuff5ev7WdQPgnkJmzcqbHdvnH/ysLSleQYZJnnBDyAFNS1Jmffnc9dHN/bUu4Qtg==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@hazae41/cubane/-/cubane-0.0.12.tgz", + "integrity": "sha512-IOH5Yu71L6/fsmfvARm8ELRvZ7b2/qIknTdTzyRs1bFXJthTlcOKS8jwp+nuzHPXOGgCfvAcOwod903EH+JK6Q==", "dependencies": { "@hazae41/base16": "^1.0.9", "@hazae41/binary": "^1.3.1", + "@hazae41/box": "^1.0.3", "@hazae41/bytes": "^1.2.2", "@hazae41/cursor": "^1.1.26", "@hazae41/keccak256": "^1.0.3", - "@hazae41/result": "^1.1.4" + "@hazae41/result": "^1.1.4", + "idna-uts46-hx": "^5.1.0" } }, "node_modules/@hazae41/cursor": { @@ -3570,9 +3572,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.8", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", - "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.10.tgz", + "integrity": "sha512-5VEC5RgXIk1HHdyN1pHlg0cOqnxHzvPGpMMyGAP5qSaDRmyZNDaQ0kkVAkK6NYlDhP6YBID3llaXlmAS/mdgCA==", "dev": true, "dependencies": { "@types/react": "*" diff --git a/package.json b/package.json index c30087d3..e86e580e 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@hazae41/cascade": "^1.1.27", "@hazae41/chacha20poly1305": "^1.0.2", "@hazae41/cleaner": "^2.0.3", - "@hazae41/cubane": "^0.0.11", + "@hazae41/cubane": "^0.0.12", "@hazae41/cursor": "^1.1.26", "@hazae41/echalote": "^0.2.80", "@hazae41/ed25519": "^2.1.6", @@ -78,7 +78,7 @@ "@types/chrome": "^0.0.246", "@types/node": "20.8.2", "@types/react": "18.2.24", - "@types/react-dom": "18.2.8", + "@types/react-dom": "18.2.10", "@types/w3c-web-usb": "^1.0.7", "autoprefixer": "^10.4.16", "eslint": "8.50.0", diff --git a/src/libs/ethereum/mods/ens/ens.ts b/src/libs/ethereum/mods/ens/ens.ts deleted file mode 100644 index bdadadf2..00000000 --- a/src/libs/ethereum/mods/ens/ens.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Base16 } from "@hazae41/base16" -import { Box } from "@hazae41/box" -import { Bytes } from "@hazae41/bytes" -import { ZeroHexString } from "@hazae41/cubane" -import { Copiable, Copied, Keccak256 } from "@hazae41/keccak256" -import * as Uts46 from "idna-uts46-hx" - -export class Slot implements Disposable { - - constructor( - public inner: T - ) { } - - [Symbol.dispose]() { - this.inner[Symbol.dispose]() - } - -} - -export namespace Ens { - - export function namehash(name: string): ZeroHexString { - if (name.length === 0) - return "0x".padEnd(2 + 32, "0") as ZeroHexString - - const uts46 = Uts46.toUnicode(name, { useStd3ASCII: true }) - const labels = uts46.split('.').reverse() - - using node: Slot> = new Slot(new Box(new Copied(new Uint8Array(32)))) - - for (const label of labels) { - using previous = node.inner.unwrap() - using hashed = Keccak256.get().tryHash(Bytes.fromUtf8(label)).unwrap() - - const concat = Bytes.concat([previous.bytes, hashed.bytes]) - node.inner = new Box(Keccak256.get().tryHash(concat).unwrap()) - } - - return "0x" + Base16.get().tryEncode(node.inner.unwrap().bytes).unwrap() as ZeroHexString - } - -} \ No newline at end of file diff --git a/src/mods/background/service_worker/entities/wallets/data.ts b/src/mods/background/service_worker/entities/wallets/data.ts index e6c7c554..62c8ec70 100644 --- a/src/mods/background/service_worker/entities/wallets/data.ts +++ b/src/mods/background/service_worker/entities/wallets/data.ts @@ -557,7 +557,7 @@ export function getTokenBalance(ethereum: EthereumContext, account: string, toke }).then(r => r.throw(t)) if (fetched.isErr()) - return new Ok(new Fail(fetched.inner)) + return new Ok(fetched) const returns = Cubane.Abi.createStaticBigUint(32) const balance = Cubane.Abi.tryDecode(returns, fetched.inner).throw(t).value @@ -612,4 +612,68 @@ export function getTokenBalance(ethereum: EthereumContext, account: string, toke indexer, storage }) +} + +async function tryFetchResolver(ethereum: EthereumContext, namehash: Uint8Array): Promise, Error>> { + return await Result.unthrow(async t => { + const registry = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" + + const signature = Cubane.Abi.FunctionSignature.tryParse("resolver(bytes32)").throw(t) + const data = Cubane.Abi.tryEncode(signature.args.from(namehash)).throw(t) + + const fetched = await tryEthereumFetch(ethereum, { + method: "eth_call", + params: [{ + to: registry, + data: data + }, "pending"] + }).then(r => r.throw(t)) + + if (fetched.isErr()) + return new Ok(fetched) + + const returns = Cubane.Abi.StaticAddress + const address = Cubane.Abi.tryDecode(returns, fetched.inner).throw(t).value + + return new Ok(new Data(address)) + }) +} + +export function getENS(ethereum: EthereumContext, name: string, storage: IDBStorage) { + const fetcher = () => Result.unthrow, Error>>(async t => { + const namehash = Cubane.Ens.tryNamehash(name).throw(t) + const resolver = await tryFetchResolver(ethereum, namehash).then(r => r.throw(t)) + + if (resolver.isErr()) + return new Ok(resolver) + + const signature = Cubane.Abi.FunctionSignature.tryParse("addr(bytes32)").throw(t) + const data = Cubane.Abi.tryEncode(signature.args.from(namehash)).throw(t) + + const fetched = await tryEthereumFetch(ethereum, { + method: "eth_call", + params: [{ + to: resolver.inner, + data: data + }, "pending"] + }).then(r => r.throw(t)) + + if (fetched.isErr()) + return new Ok(fetched) + + const returns = Cubane.Abi.StaticAddress + const address = Cubane.Abi.tryDecode(returns, fetched.inner).throw(t).value + + return new Ok(new Data(address)) + }) + + return createQuery, ZeroHexString, Error>({ + key: { + chainId: 1, + method: "eth_resolveEns", + params: [name] + }, + fetcher, + storage + }) } \ No newline at end of file diff --git a/src/mods/background/service_worker/index.ts b/src/mods/background/service_worker/index.ts index 5b90d110..f47d6116 100644 --- a/src/mods/background/service_worker/index.ts +++ b/src/mods/background/service_worker/index.ts @@ -22,6 +22,7 @@ import { Bytes } from "@hazae41/bytes" import { Cadenas } from "@hazae41/cadenas" import { ChaCha20Poly1305 } from "@hazae41/chacha20poly1305" import { Disposer } from "@hazae41/cleaner" +import { ZeroHexString } from "@hazae41/cubane" import { Circuit, Echalote, Fallback, TorClientDuplex } from "@hazae41/echalote" import { Ed25519 } from "@hazae41/ed25519" import { Fleche } from "@hazae41/fleche" @@ -52,7 +53,7 @@ import { ExSessionData, Session, SessionByOrigin, SessionData, SessionRef, WcSes import { Status, StatusData } from "./entities/sessions/status/data" import { Users } from "./entities/users/all/data" import { User, UserData, UserInit, UserSession, getCurrentUser } from "./entities/users/data" -import { EthereumContext, EthereumQueryKey, Wallet, WalletData, WalletRef, getBalance, getEthereumUnknown, getPairPrice, getTokenBalance, tryEthereumFetch } from "./entities/wallets/data" +import { EthereumContext, EthereumQueryKey, Wallet, WalletData, WalletRef, getBalance, getENS, getEthereumUnknown, getPairPrice, getTokenBalance, tryEthereumFetch } from "./entities/wallets/data" import { tryCreateUserStorage } from "./storage" declare global { @@ -648,6 +649,16 @@ export class Global { }) } + async makeEthereumResolveEns(ethereum: EthereumContext, request: RpcRequestPreinit, storage: IDBStorage): Promise, ZeroHexString, Error>, Error>> { + return await Result.unthrow(async t => { + const [name] = (request as RpcRequestPreinit<[string]>).params + + const query = getENS(ethereum, name, storage) + + return new Ok(query) + }) + } + async eth_sendTransaction(ethereum: EthereumContext, request: RpcRequestPreinit, mouse?: Mouse): Promise> { return await Result.unthrow(async t => { const [{ from, to, gas, value, data }] = (request as RpcRequestPreinit<[{ @@ -1058,13 +1069,15 @@ export class Global { return new Ok(getEthereumUnknown(ethereum, request, storage)) } - async routeAndMakeEthereum(ethereum: EthereumContext, request: RpcRequestPreinit, storage: IDBStorage): Promise, Error>> { + async routeAndMakeEthereum(ethereum: EthereumContext, request: RpcRequestPreinit, storage: IDBStorage): Promise, Error>> { if (request.method === "eth_getBalance") return await this.makeEthereumBalance(ethereum, request, storage) if (request.method === "eth_getTokenBalance") return await this.makeEthereumTokenBalance(ethereum, request, storage) if (request.method === "eth_getPairPrice") return await this.makeEthereumPairPrice(ethereum, request, storage) + if (request.method === "eth_resolveEns") + return await this.makeEthereumResolveEns(ethereum, request, storage) return await this.makeEthereumUnknown(ethereum, request, storage) } diff --git a/src/mods/foreground/entities/wallets/actions/send/contract.tsx b/src/mods/foreground/entities/wallets/actions/send/contract.tsx index e3b35c61..38e6ad1b 100644 --- a/src/mods/foreground/entities/wallets/actions/send/contract.tsx +++ b/src/mods/foreground/entities/wallets/actions/send/contract.tsx @@ -1,6 +1,6 @@ import { BigInts } from "@/libs/bigints/bigints"; import { UIError } from "@/libs/errors/errors"; -import { ContractTokenInfo } from "@/libs/ethereum/mods/chain"; +import { ContractTokenInfo, chainByChainId, chainIdByName } from "@/libs/ethereum/mods/chain"; import { Fixed } from "@/libs/fixed/fixed"; import { Radix } from "@/libs/hex/hex"; import { Outline } from "@/libs/icons/icons"; @@ -19,12 +19,14 @@ import { Ok, Result } from "@hazae41/result"; import { Transaction, ethers } from "ethers"; import { useDeferredValue, useMemo, useState } from "react"; import { useWalletData } from "../../context"; -import { EthereumContextProps, EthereumWalletInstance, useGasPrice, useNonce, useTokenBalance } from "../../data"; +import { EthereumContextProps, EthereumWalletInstance, useENS, useEthereumContext, useGasPrice, useNonce, useTokenBalance } from "../../data"; export function WalletDataSendContractTokenDialog(props: TitleProps & CloseProps & EthereumContextProps & { token: ContractTokenInfo }) { const wallet = useWalletData() const { title, context, token, close } = props + const mainnet = useEthereumContext(wallet, chainByChainId[chainIdByName.ETHEREUM]) + const balanceQuery = useTokenBalance(wallet.address, token, context, []) const maybeBalance = balanceQuery.data?.inner @@ -42,6 +44,15 @@ export function WalletDataSendContractTokenDialog(props: TitleProps & CloseProps setRawRecipientInput(e.currentTarget.value) }, []) + const maybeEnsName = defRecipientInput.endsWith(".eth") + ? defRecipientInput + : undefined + const ensAddressQuery = useENS(mainnet, maybeEnsName) + + const maybeFinalAddress = defRecipientInput.endsWith(".eth") + ? ensAddressQuery.data?.inner + : defRecipientInput + const RecipientInput = <>
Recipient @@ -49,7 +60,7 @@ export function WalletDataSendContractTokenDialog(props: TitleProps & CloseProps
@@ -109,9 +120,13 @@ export function WalletDataSendContractTokenDialog(props: TitleProps & CloseProps return new UIError(`Could not fetch or parse nonce`) }).throw(t) + const address = Option.wrap(maybeFinalAddress).okOrElseSync(() => { + return new UIError(`Could not fetch or parse address`) + }).throw(t) + const signature = Cubane.Abi.FunctionSignature.tryParse("transfer(address,uint256)").throw(t) const fixed = Fixed.fromDecimalString(defValueInput, token.decimals) - const args = signature.args.from(ethers.getAddress(defRecipientInput), fixed.value) + const args = signature.args.from(ethers.getAddress(address), fixed.value) const data = Cubane.Abi.tryEncode(args).throw(t) const gas = await context.background.tryRequest({ @@ -158,7 +173,7 @@ export function WalletDataSendContractTokenDialog(props: TitleProps & CloseProps return Ok.void() }).then(Results.logAndAlert) - }, [context, wallet, maybeGasPrice, maybeFinalNonce, defRecipientInput, defValueInput]) + }, [context, wallet, maybeGasPrice, maybeFinalNonce, maybeFinalAddress, defValueInput]) const TxHashDisplay = <>
diff --git a/src/mods/foreground/entities/wallets/actions/send/native.tsx b/src/mods/foreground/entities/wallets/actions/send/native.tsx index 97eb186a..8b12fddb 100644 --- a/src/mods/foreground/entities/wallets/actions/send/native.tsx +++ b/src/mods/foreground/entities/wallets/actions/send/native.tsx @@ -1,5 +1,6 @@ import { BigInts } from "@/libs/bigints/bigints"; import { UIError } from "@/libs/errors/errors"; +import { chainByChainId, chainIdByName } from "@/libs/ethereum/mods/chain"; import { Fixed } from "@/libs/fixed/fixed"; import { Radix } from "@/libs/hex/hex"; import { Outline } from "@/libs/icons/icons"; @@ -17,12 +18,14 @@ import { Ok, Result } from "@hazae41/result"; import { Transaction, ethers } from "ethers"; import { useDeferredValue, useMemo, useState } from "react"; import { useWalletData } from "../../context"; -import { EthereumContextProps, EthereumWalletInstance, useBalance, useGasPrice, useNonce } from "../../data"; +import { EthereumContextProps, EthereumWalletInstance, useBalance, useENS, useEthereumContext, useGasPrice, useNonce } from "../../data"; export function WalletDataSendNativeTokenDialog(props: TitleProps & CloseProps & EthereumContextProps) { const wallet = useWalletData() const { title, context, close } = props + const mainnet = useEthereumContext(wallet, chainByChainId[chainIdByName.ETHEREUM]) + const balanceQuery = useBalance(wallet.address, context, []) const maybeBalance = balanceQuery.data?.inner @@ -40,6 +43,15 @@ export function WalletDataSendNativeTokenDialog(props: TitleProps & CloseProps & setRawRecipientInput(e.currentTarget.value) }, []) + const maybeEnsName = defRecipientInput.endsWith(".eth") + ? defRecipientInput + : undefined + const ensAddressQuery = useENS(mainnet, maybeEnsName) + + const maybeFinalAddress = defRecipientInput.endsWith(".eth") + ? ensAddressQuery.data?.inner + : defRecipientInput + const RecipientInput = <>
Recipient @@ -47,7 +59,7 @@ export function WalletDataSendNativeTokenDialog(props: TitleProps & CloseProps &
@@ -107,6 +119,10 @@ export function WalletDataSendNativeTokenDialog(props: TitleProps & CloseProps & return new UIError(`Could not fetch or parse nonce`) }).throw(t) + const address = Option.wrap(maybeFinalAddress).okOrElseSync(() => { + return new UIError(`Could not fetch or parse address`) + }).throw(t) + const gas = await context.background.tryRequest({ method: "brume_eth_fetch", params: [context.wallet.uuid, context.chain.chainId, { @@ -114,7 +130,7 @@ export function WalletDataSendNativeTokenDialog(props: TitleProps & CloseProps & params: [{ chainId: Radix.toZeroHex(context.chain.chainId), from: wallet.address, - to: ethers.getAddress(defRecipientInput), + to: ethers.getAddress(address), gasPrice: Radix.toZeroHex(gasPrice), value: Radix.toZeroHex(Fixed.fromDecimalString(defValueInput, 18).value), nonce: Radix.toZeroHex(nonce) @@ -124,7 +140,7 @@ export function WalletDataSendNativeTokenDialog(props: TitleProps & CloseProps & const tx = Result.runAndDoubleWrapSync(() => { return Transaction.from({ - to: ethers.getAddress(defRecipientInput), + to: ethers.getAddress(address), gasLimit: gas, chainId: context.chain.chainId, gasPrice: gasPrice, @@ -151,7 +167,7 @@ export function WalletDataSendNativeTokenDialog(props: TitleProps & CloseProps & return Ok.void() }).then(Results.logAndAlert) - }, [context, wallet, maybeGasPrice, maybeFinalNonce, defRecipientInput, defValueInput]) + }, [context, wallet, maybeGasPrice, maybeFinalNonce, maybeFinalAddress, defValueInput]) const TxHashDisplay = <>
diff --git a/src/mods/foreground/entities/wallets/data.ts b/src/mods/foreground/entities/wallets/data.ts index 598bdf62..ee1bf262 100644 --- a/src/mods/foreground/entities/wallets/data.ts +++ b/src/mods/foreground/entities/wallets/data.ts @@ -8,7 +8,7 @@ import { Seed } from "@/mods/background/service_worker/entities/seeds/data" import { EthereumAuthPrivateKeyWalletData, EthereumQueryKey, EthereumSeededWalletData, EthereumUnauthPrivateKeyWalletData, EthereumWalletData, Wallet, WalletData } from "@/mods/background/service_worker/entities/wallets/data" import { Base16 } from "@hazae41/base16" import { Base64 } from "@hazae41/base64" -import { Abi } from "@hazae41/cubane" +import { Abi, ZeroHexString } from "@hazae41/cubane" import { Data, Fetched, FetcherMore, createQuery, useError, useFallback, useFetch, useQuery, useVisible } from "@hazae41/glacier" import { RpcRequestPreinit, RpcResponse } from "@hazae41/jsonrpc" import { Nullable, Option } from "@hazae41/option" @@ -514,3 +514,32 @@ export function usePairPrice(ethereum: EthereumContext, pair: PairInfo) { useError(query, console.error) return query } + + +export function getENS(context: EthereumContext, name: Nullable, storage: UserStorage) { + if (name == null) + return undefined + + const fetcher = async (request: RpcRequestPreinit, more: FetcherMore = {}) => + await tryFetch(request, context) + + return createQuery, ZeroHexString, Error>({ + key: { + chainId: 1, + method: "eth_resolveEns", + params: [name] + }, + fetcher, + storage + }) +} + +export function useENS(ethereum: EthereumContext, name: Nullable) { + const storage = useUserStorage().unwrap() + const query = useQuery(getENS, [ethereum, name, storage]) + useFetch(query) + useVisible(query) + useSubscribe(query, storage) + useError(query, console.error) + return query +} \ No newline at end of file