Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(evm): add dynamic #309

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions apps/bob-pay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@yudiel/react-qr-scanner": "^2.0.4",
"@zerodev/sdk": "^5.3.4",
"big.js": "catalog:",
"bitcoin-address-validation": "^2.2.3",
"date-fns": "catalog:",
"graphql-request": "catalog:",
"negotiator": "catalog:",
Expand Down
4 changes: 2 additions & 2 deletions apps/bob-pay/src/lib/form/yup.custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import Big from 'big.js';
import * as yup from 'yup';
import { AnyObject, Maybe } from 'yup/lib/types';
import { isValidBTCAddress, BitcoinNetwork } from '@gobob/utils';
import { validate, Network as BitcoinNetwork } from 'bitcoin-address-validation';
import { isAddress } from 'viem';

yup.addMethod<yup.StringSchema>(yup.string, 'requiredAmount', function (action: string, customMessage?: string) {
Expand Down Expand Up @@ -73,7 +73,7 @@ yup.addMethod<yup.StringSchema>(

yup.addMethod<yup.StringSchema>(yup.string, 'btcAddress', function (network: BitcoinNetwork, customMessage?: string) {
return this.test('btcAddress', (value, ctx) => {
if (!value || !isValidBTCAddress(value, network)) {
if (!value || !validate(value, network)) {
const message = customMessage || 'Please enter a valid address';

return ctx.createError({ message });
Expand Down
20 changes: 15 additions & 5 deletions apps/evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@
},
"dependencies": {
"@binance/w3w-wagmi-connector-v2": "1.2.4-alpha.0",
"@dynamic-labs/bitcoin": "^3.6.2",
"@dynamic-labs/ethereum": "^3.6.2",
"@dynamic-labs/iconic": "^3.6.2",
"@dynamic-labs/sdk-react-core": "^3.6.2",
"@dynamic-labs/wagmi-connector": "^3.6.2",
"@dynamic-labs/wallet-book": "^3.8.5",
"@gobob/bob-sdk": "^3.1.0",
"@gobob/chains": "workspace:^",
"@gobob/currency": "workspace:^",
"@gobob/hooks": "workspace:^",
"@gobob/icons": "workspace:^",
"@gobob/sats-wagmi": "workspace:^",
"@gobob/tokens": "workspace:^",
"@gobob/ui": "workspace:^",
"@gobob/utils": "workspace:^",
Expand All @@ -33,29 +38,34 @@
"@react-aria/interactions": "catalog:",
"@react-aria/label": "^3.7.6",
"@react-aria/utils": "catalog:",
"@scure/base": "^1.1.9",
"@scure/btc-signer": "^1.4.0",
"@sentry/nextjs": "catalog:",
"@tanstack/react-store": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-store": "catalog:",
"@vercel/kv": "catalog:",
"@wagmi/core": "catalog:",
"big.js": "catalog:",
"bitcoin-address-validation": "catalog:",
"boring-avatars": "^1.11.2",
"date-fns": "catalog:",
"graphql-request": "catalog:",
"lottie-react": "^2.4.0",
"negotiator": "catalog:",
"next": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-jazzicon": "^1.0.4",
"react-multi-carousel": "^2.8.5",
"react-otp-input": "^3.1.1",
"react-qr-code": "^2.0.12",
"siwe": "^2.1.4",
"styled-components": "catalog:",
"use-count-up": "^3.0.1",
"usehooks-ts": "catalog:",
"vaul": "^1.1.1",
"viem": "catalog:",
"yup": "catalog:",
"wagmi": "catalog:"
"wagmi": "catalog:",
"yup": "catalog:"
},
"devDependencies": {
"@gobob/test-utils": "workspace:^",
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { BridgeAlert } from './BridgeAlert';

import { l1StandardBridgeAbi } from '@/abis/L1StandardBridge.abi';
import { l2StandardBridgeAbi } from '@/abis/L2StandardBridge.abi';
import { AuthButton } from '@/connect-ui';
import { AuthButton } from '@/components';
import { L1_CHAIN, L2_CHAIN } from '@/constants';
import { bridgeContracts } from '@/constants/bridge';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useAccount } from 'wagmi';
import { BtcTokenInput, GatewayGasSwitch, GatewayTransactionDetails } from '../../../components';
import { useGateway, useGatewayForm } from '../../../hooks';

import { AuthButton } from '@/connect-ui';
import { AuthButton } from '@/components';
import { isProd } from '@/constants';
import { TokenData } from '@/hooks';
import { BRIDGE_RECIPIENT, BridgeFormValues } from '@/lib/form/bridge';
Expand Down
5 changes: 2 additions & 3 deletions apps/evm/src/app/[lang]/(bridge)/bridge/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Metadata } from 'next';
import { t } from '@lingui/macro';
import { Metadata } from 'next';

import { Bridge } from './Bridge';

import { withLinguiPage } from '@/i18n/withLigui';
import { getI18nInstance } from '@/i18n/appRouterI18n';
import { PageLangParam } from '@/i18n/withLigui';
import { PageLangParam, withLinguiPage } from '@/i18n/withLigui';

export function generateMetadata({ params }: PageLangParam): Metadata {
const i18n = getI18nInstance(params.lang);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { P } from '@gobob/ui';
import styled from 'styled-components';

import { AuthButton } from '@/connect-ui';
import { AuthButton } from '@/components';

const StyledTimePill = styled(P)`
padding: ${({ theme }) => `${theme.spacing('xs')} ${theme.spacing('lg')}`};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Currency, CurrencyAmount } from '@gobob/currency';
import { useAccount as useSatsAccount } from '@gobob/sats-wagmi';
import { BITCOIN } from '@gobob/tokens';
import {
Alert,
Expand Down Expand Up @@ -27,6 +26,7 @@ import { GatewayFeeSettingsModal } from './GatewayFeeSettingsModal';
import { StyledDlGroup, StyledDt } from './GatewayTransactionDetails.style';

import { AmountLabel } from '@/components';
import { useBtcAccount } from '@/hooks';
import { GatewayTransactionType } from '@/types';

type GatewayTransactionDetailsProps = {
Expand All @@ -50,7 +50,7 @@ const GatewayTransactionDetails = ({
}: GatewayTransactionDetailsProps): JSX.Element => {
const { i18n } = useLingui();

const { address: btcAddress } = useSatsAccount();
const { address: btcAddress } = useBtcAccount();

const [isOpen, setOpen] = useState(false);

Expand Down
10 changes: 6 additions & 4 deletions apps/evm/src/app/[lang]/(bridge)/hooks/tests/useGateway.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useAccount as useSatsAccount } from '@gobob/sats-wagmi';
import { act, renderHook } from '@testing-library/react-hooks';
import { describe, expect, it, Mock, vi } from 'vitest';
import { useAccount } from 'wagmi';

import { useGateway } from '../useGateway';

import { useBtcAccount, useBtcSignAllInputs } from '@/hooks';
import { wrapper } from '@/test-utils';
import { GatewayTransactionType } from '@/types';

Expand All @@ -17,12 +17,13 @@ vi.mock(import('wagmi'), async (importOriginal) => {
};
});

vi.mock(import('@gobob/sats-wagmi'), async (importOriginal) => {
vi.mock(import('@/hooks'), async (importOriginal) => {
const actual = await importOriginal();

return {
...actual,
useAccount: vi.fn()
useBtcAccount: vi.fn(),
useBtcSignAllInputs: vi.fn()
};
});

Expand All @@ -31,7 +32,8 @@ describe('useGateway', () => {
vi.clearAllMocks();

(useAccount as Mock).mockReturnValue({ address: '0x123456789abcdef' });
(useSatsAccount as Mock).mockReturnValue({ connector: vi.fn() });
(useBtcAccount as Mock).mockReturnValue({ address: 'bc1qaddress' });
(useBtcSignAllInputs as Mock).mockReturnValue({ mutateAsync: () => '0x0001111000' });
});

it('should initialize correctly with default settings', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useAccount as useSatsAccount } from '@gobob/sats-wagmi';
import { act, renderHook } from '@testing-library/react-hooks';
import { Mock, vi } from 'vitest';
import { useAccount } from 'wagmi';

import { useGatewayForm } from '../useGatewayForm';

import { useIsContract } from '@/hooks';
import { useBtcAccount, useIsContract } from '@/hooks';
import { BRIDGE_AMOUNT, BRIDGE_ASSET, BRIDGE_RECIPIENT } from '@/lib/form/bridge';

vi.mock(import('wagmi'), async (importOriginal) => {
Expand All @@ -26,12 +25,12 @@ vi.mock(import('@/hooks'), async (importOriginal) => {
};
});

vi.mock(import('@gobob/sats-wagmi'), async (importOriginal) => {
vi.mock(import('@/hooks'), async (importOriginal) => {
const actual = await importOriginal();

return {
...actual,
useAccount: vi.fn()
useBtcAccount: vi.fn()
};
});

Expand All @@ -48,7 +47,7 @@ describe('useGatewayForm', () => {
beforeEach(() => {
(useAccount as Mock).mockReturnValue({ address: '0x123' });
(useIsContract as Mock).mockReturnValue({ isContract: false });
(useSatsAccount as Mock).mockReturnValue({ address: 'bc1qaddress' });
(useBtcAccount as Mock).mockReturnValue({ address: 'bc1qaddress' });
});

it('should initialize with correct default values', () => {
Expand Down
47 changes: 27 additions & 20 deletions apps/evm/src/app/[lang]/(bridge)/hooks/useGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@

import { GatewayQuoteParams } from '@gobob/bob-sdk';
import { Bitcoin, CurrencyAmount, ERC20Token } from '@gobob/currency';
import {
BtcAddressType,
FeeRateReturnType,
useAccount as useSatsAccount,
useBalance as useSatsBalance,
useFeeEstimate as useSatsFeeEstimate,
useFeeRate as useSatsFeeRate
} from '@gobob/sats-wagmi';
import { BITCOIN } from '@gobob/tokens';
import { toast } from '@gobob/ui';
import { t } from '@lingui/macro';
Expand All @@ -27,8 +19,17 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } f
import { DebouncedState, useDebounceValue } from 'usehooks-ts';
import { Address, isAddress } from 'viem';
import { useAccount } from 'wagmi';
import { AddressType } from 'bitcoin-address-validation';

import { INTERVAL } from '@/constants';
import {
BtcFeeRateReturnType,
useBtcAccount,
useBtcBalance,
useBtcFeeEstimate,
useBtcFeeRate,
useBtcSignAllInputs
} from '@/hooks';
import { gatewaySDK } from '@/lib/bob-sdk';
import { bridgeKeys } from '@/lib/react-query';
import {
Expand Down Expand Up @@ -80,7 +81,7 @@ const getBalanceAmount = (
return availableBalance;
};

const feeRatesSelect = ({ esplora, memPool }: FeeRateReturnType): GatewayTransactionSpeedData => {
const feeRatesSelect = ({ esplora, memPool }: BtcFeeRateReturnType): GatewayTransactionSpeedData => {
return {
fastest: Math.ceil(Math.min(esplora[2], memPool.fastestFee)),
fast: Math.ceil(Math.min(esplora[4], memPool.halfHourFee)),
Expand Down Expand Up @@ -163,8 +164,8 @@ const useGateway = ({ params, onError, onMutate, onSuccess }: UseGatewayLiquidit

const { address: evmAddress } = useAccount();

const { address: btcAddress, connector: satsConnector, addressType: btcAddressType } = useSatsAccount();
const { data: satsBalance } = useSatsBalance();
const { address: btcAddress, publicKey: btcPublicKey, addressType: btcAddressType } = useBtcAccount();
const { data: satsBalance } = useBtcBalance();

const [isTopUpEnabled, setTopUpEnabled] = useState(true);
const [selectedFee, setSelectedFee] = useState<GatewayTransactionFee>({ speed: GatewayTransactionSpeed.SLOW });
Expand All @@ -173,7 +174,7 @@ const useGateway = ({ params, onError, onMutate, onSuccess }: UseGatewayLiquidit

const minAmount = useMemo(() => getMinAmount(isTopUpEnabled), [isTopUpEnabled]);

const isTapRootAddress = btcAddressType === BtcAddressType.p2tr;
const isTapRootAddress = btcAddressType === AddressType.p2tr;

const liquidityQueryEnabled = Boolean(
params.toChain && params.toToken && params.type === GatewayTransactionType.STAKE ? params.strategyAddress : true
Expand Down Expand Up @@ -211,7 +212,7 @@ const useGateway = ({ params, onError, onMutate, onSuccess }: UseGatewayLiquidit
}
});

const feeRatesQueryResult = useSatsFeeRate({
const feeRatesQueryResult = useBtcFeeRate({
query: {
select: feeRatesSelect
}
Expand All @@ -220,12 +221,12 @@ const useGateway = ({ params, onError, onMutate, onSuccess }: UseGatewayLiquidit
const feeRate =
selectedFee.speed === 'custom' ? selectedFee.networkRate : feeRatesQueryResult.data?.[selectedFee.speed];

const feeEstimateQueryResult = useSatsFeeEstimate({
const feeEstimateQueryResult = useBtcFeeEstimate({
opReturnData: evmAddress,
feeRate: feeRate,
query: {
enabled: Boolean(satsBalance && satsBalance.total > 0n && evmAddress),
select: (data) => CurrencyAmount.fromRawAmount(BITCOIN, data.amount)
select: (data) => CurrencyAmount.fromRawAmount(BITCOIN, data.amount || 0)
}
});

Expand Down Expand Up @@ -306,10 +307,12 @@ const useGateway = ({ params, onError, onMutate, onSuccess }: UseGatewayLiquidit
}
});

const { mutateAsync: signAllInputsAsync } = useBtcSignAllInputs();

const mutation = useMutation({
mutationKey: bridgeKeys.btcDeposit(evmAddress, btcAddress),
mutationFn: async ({ evmAddress }: { evmAddress: Address | string }): Promise<InitGatewayTransaction> => {
if (!satsConnector) {
if (!btcAddress || !btcPublicKey) {
throw new Error('Connector missing');
}

Expand All @@ -322,7 +325,7 @@ const useGateway = ({ params, onError, onMutate, onSuccess }: UseGatewayLiquidit
}

if (!params.toChain) {
throw new Error('Something went wrong');
throw new Error('Missing toChain');
}

if (!isAddress(evmAddress)) {
Expand All @@ -345,13 +348,17 @@ const useGateway = ({ params, onError, onMutate, onSuccess }: UseGatewayLiquidit
...DEFAULT_GATEWAY_QUOTE_PARAMS,
toChain: params.toChain,
toUserAddress: evmAddress,
fromUserAddress: satsConnector.paymentAddress!,
fromUserPublicKey: satsConnector.publicKey,
fromUserAddress: btcAddress,
fromUserPublicKey: btcPublicKey,
gasRefill: isTopUpEnabled ? GAS_REFILL : 0,
feeRate
});

const bitcoinTxHex = await satsConnector.signAllInputs(psbtBase64!);
if (!psbtBase64) {
throw new Error('Failed to start order');
}

const bitcoinTxHex = await signAllInputsAsync(psbtBase64);

// NOTE: user does not broadcast the tx, that is done by
// the relayer after it is validated
Expand Down
5 changes: 2 additions & 3 deletions apps/evm/src/app/[lang]/(bridge)/hooks/useGatewayForm.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
'use client';

import { useAccount as useSatsAccount } from '@gobob/sats-wagmi';
import { useForm } from '@gobob/ui';
import Big from 'big.js';
import { useEffect } from 'react';
import { useAccount } from 'wagmi';

import { UseGatewayQueryDataReturnType } from './useGateway';

import { useIsContract } from '@/hooks';
import { useBtcAccount, useIsContract } from '@/hooks';
import {
BRIDGE_AMOUNT,
BRIDGE_ASSET,
Expand All @@ -31,7 +30,7 @@ const useGatewayForm = ({ query, defaultAsset, onSubmit }: UseGatewayFormProps)

const { isContract: isSmartAccount } = useIsContract({ address: evmAddress });

const { address: btcAddress } = useSatsAccount();
const { address: btcAddress } = useBtcAccount();

useEffect(() => {
if (!query.fee.estimate.data || !form.values[BRIDGE_AMOUNT]) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useGateway, useGatewayForm } from '../../../hooks';

import { StrategyData } from './StakeForm';

import { AuthButton } from '@/connect-ui';
import { AuthButton } from '@/components';
import { BRIDGE_RECIPIENT, BridgeFormValues } from '@/lib/form/bridge';
import { GatewayTransactionType, InitGatewayTransaction } from '@/types';

Expand Down
5 changes: 0 additions & 5 deletions apps/evm/src/app/[lang]/index.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
@font-face {
font-family: eurostar;
src: url(/assets/fonts/eurostar-black-extended.ttf);
}

:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
Expand Down
Loading