Skip to content

Commit

Permalink
feature: bob x interlay
Browse files Browse the repository at this point in the history
  • Loading branch information
tomjeatt committed Feb 26, 2025
1 parent 2227b09 commit 8eb69f8
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 56 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"redux": "^4.0.5",
"styled-components": "^5.3.5",
"typescript": "4.3.2",
"viem": "^2.21.34",
"web-vitals": "^1.0.1",
"yup": "^0.32.11"
},
Expand Down
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as constants from './constants';
import { FeatureFlags, useFeatureFlag } from './hooks/use-feature-flag';

const BTC = React.lazy(() => import(/* webpackChunkName: 'btc' */ '@/pages/BTC'));
const BOB = React.lazy(() => import(/* webpackChunkName: 'bob' */ '@/pages/BOB'));
const Strategies = React.lazy(() => import(/* webpackChunkName: 'strategies' */ '@/pages/Strategies'));
const Strategy = React.lazy(() => import(/* webpackChunkName: 'strategy' */ '@/pages/Strategies/Strategy'));
const SendAndReceive = React.lazy(() => import(/* webpackChunkName: 'sendAndReceive' */ '@/pages/SendAndReceive'));
Expand Down Expand Up @@ -119,6 +120,9 @@ const App = (): JSX.Element => {
<Route path={PAGES.WALLET}>
<Wallet />
</Route>
<Route path={PAGES.BOB}>
<BOB />
</Route>
{isStrategiesEnabled && (
<>
<Route exact path={PAGES.STRATEGIES}>
Expand Down
20 changes: 20 additions & 0 deletions src/lib/form/schemas/bob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import i18n from 'i18next';

import yup, { AddressType } from '../yup.custom';

const BOB_RECIPIENT_FIELD = 'bob-destination';

type BobFormData = {
[BOB_RECIPIENT_FIELD]?: string;
};

const bobSchema = (): yup.ObjectSchema<any> =>
yup.object().shape({
[BOB_RECIPIENT_FIELD]: yup
.string()
.required(i18n.t('forms.please_enter_your_field', { field: 'recipient' }))
.address(AddressType.ETHEREUM)
});

export { BOB_RECIPIENT_FIELD, bobSchema };
export type { BobFormData };
1 change: 1 addition & 0 deletions src/lib/form/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './bob';
export * from './btc';
export * from './loans';
export * from './pools';
Expand Down
5 changes: 4 additions & 1 deletion src/lib/form/validate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { decodeAddress, encodeAddress } from '@polkadot/keyring';
import { hexToU8a, isHex } from '@polkadot/util';
import { isAddress } from 'viem';

import { BTC_ADDRESS_REGEX } from '@/constants';

Expand All @@ -8,6 +9,8 @@ const btcAddressRegex = new RegExp(BTC_ADDRESS_REGEX);
// TODO: use library instead
const isValidBTCAddress = (address: string): boolean => btcAddressRegex.test(address);

const isValidEthereumAddress = (address: string): boolean => isAddress(address);

const isValidRelayAddress = (address: string): boolean => {
try {
encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address));
Expand All @@ -18,4 +21,4 @@ const isValidRelayAddress = (address: string): boolean => {
}
};

export { isValidBTCAddress, isValidRelayAddress };
export { isValidBTCAddress, isValidEthereumAddress, isValidRelayAddress };
8 changes: 5 additions & 3 deletions src/lib/form/yup.custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import i18n from 'i18next';
import * as yup from 'yup';
import { AnyObject, Maybe } from 'yup/lib/types';

import { isValidBTCAddress, isValidRelayAddress } from './validate';
import { isValidBTCAddress, isValidEthereumAddress, isValidRelayAddress } from './validate';

yup.addMethod<yup.StringSchema>(yup.string, 'requiredAmount', function (action: string, customMessage?: string) {
return this.transform((value) => (isNaN(value) ? undefined : value)).test('requiredAmount', (value, ctx) => {
Expand Down Expand Up @@ -108,12 +108,14 @@ yup.addMethod<yup.StringSchema>(

enum AddressType {
RELAY_CHAIN,
BTC
BTC,
ETHEREUM
}

const addressValidationMap = {
[AddressType.RELAY_CHAIN]: isValidRelayAddress,
[AddressType.BTC]: isValidBTCAddress
[AddressType.BTC]: isValidBTCAddress,
[AddressType.ETHEREUM]: isValidEthereumAddress
};

yup.addMethod<yup.StringSchema>(
Expand Down
11 changes: 11 additions & 0 deletions src/pages/BOB/BOB.styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import styled from 'styled-components';

import { Flex } from '@/component-library';

const StyledWrapper = styled(Flex)`
max-width: 540px;
width: 100%;
margin: 0 auto;
`;

export { StyledWrapper };
202 changes: 202 additions & 0 deletions src/pages/BOB/BOB.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { mergeProps } from '@react-aria/utils';
import { useEffect, useMemo } from 'react';
import { useState } from 'react';
import { withErrorBoundary } from 'react-error-boundary';
import { useMutation } from 'react-query';

import { Card, Divider, Flex, H1, Input, P, TextLink } from '@/component-library';
import { AuthCTA, MainContainer } from '@/components';
import ErrorFallback from '@/legacy-components/ErrorFallback';
import { useForm } from '@/lib/form';
import { BOB_RECIPIENT_FIELD, BobFormData, bobSchema } from '@/lib/form/schemas';
import { KeyringPair, useSubstrateSecureState } from '@/lib/substrate';

import { NotificationToastType, useNotifications } from '../../utils/context/Notifications';
import { signMessage } from '../../utils/helpers/wallet';
import { StyledWrapper } from './BOB.styles';

const postSignature = async (account: KeyringPair, ethereumAddress: string) => {
const legalText = await fetch(`https://bob-intr-drop.interlay.workers.dev/legaltext`);

const message = await legalText.text();

const signerResult = await signMessage(account, message);

if (!signerResult?.signature) {
throw new Error('Failed to sign message');
}

return fetch(`https://bob-intr-drop.interlay.workers.dev/accept`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
accountid: account.address,
signature: signerResult?.signature,
evmAddress: ethereumAddress
})
});
};

const checkAddress = async (address: string) => {
// This checks if a given address is eligible for claiming
const response = await fetch(`https://bob-intr-drop.interlay.workers.dev/check/${address}`);

// If an address is not eligible, error code 499 is returned. We check this here
// to distinguish address not found from api errors/
if (!response.ok && response.status !== 499) {
throw new Error('There was an error checking the address');
}

return response.ok;
};

const BOB = (): JSX.Element => {
const [isEligible, setIsEligible] = useState(false);

const { selectedAccount } = useSubstrateSecureState();
const notifications = useNotifications();

const submitEthereumAddressMutation = useMutation(
(variables: { selectedAccount: KeyringPair; ethereumAddress: string }) =>
postSignature(variables.selectedAccount, variables.ethereumAddress),
{
onError: async (_, variables) => {
notifications.show(variables.selectedAccount.address, {
type: NotificationToastType.STANDARD,
props: { variant: 'error', title: 'Something went wrong. Please try again.' }
});
},
onSuccess: async (data, variables) => {
// 409 is a success response to indicate that an EVM address has already been
// submitted for the signing account so we show the user an error notification.
data.status === 409
? notifications.show(variables.selectedAccount.address, {
type: NotificationToastType.STANDARD,
props: {
variant: 'error',
title: 'Already submitted',
description: 'An EVM address has already been submitted for this account.'
}
})
: notifications.show(variables.selectedAccount.address, {
type: NotificationToastType.STANDARD,
props: { variant: 'success', title: 'Address submitted' }
});

form.resetForm();
}
}
);

const initialValues = useMemo(
() => ({
[BOB_RECIPIENT_FIELD]: ''
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

const handleSubmit = async (values: BobFormData) => {
const ethereumAddress = values[BOB_RECIPIENT_FIELD];

if (!selectedAccount || !ethereumAddress) return;

submitEthereumAddressMutation.mutate({ selectedAccount, ethereumAddress });
};

const form = useForm<BobFormData>({
initialValues,
validationSchema: bobSchema(),
onSubmit: handleSubmit
});

useEffect(() => {
if (!selectedAccount?.address) return;

const setEligibility = async () => {
const isAddressValid = await checkAddress(selectedAccount.address);

setIsEligible(isAddressValid);
};

setEligibility();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAccount?.address]);

// Reset mutation on account change
useEffect(() => {
if (submitEthereumAddressMutation.isLoading && selectedAccount?.address) {
submitEthereumAddressMutation.reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAccount?.address]);

return (
<MainContainer>
<StyledWrapper direction='column' gap='spacing8'>
<Card gap='spacing2'>
<H1 size='base' color='secondary' weight='bold' align='center'>
BOB x Interlay
</H1>
<Divider size='medium' orientation='horizontal' color='secondary' marginBottom='spacing4' />
<Flex direction='column'>
{isEligible ? (
<>
<Flex direction='column' gap='spacing4'>
<P>
Claim your{' '}
<TextLink external to='http://app.gobob.xyz' underlined>
BOB
</TextLink>{' '}
x Interlay exclusive NFT badge today.
</P>
<P>
Only original Interlay community members are eligible. Simply submit the EVM address you&apos;d like
to receive the NFT and sign the transaction with your Interlay account to prove your community
status. We&apos;ll let you know in Interlay Discord when the NFT will be available to claim.
</P>
<P>The claim page is only live until [DATE].</P>
</Flex>
<form onSubmit={form.handleSubmit}>
<Flex direction='column' gap='spacing8'>
<Flex direction='column' gap='spacing4'>
<Input
placeholder='Enter ethereum address'
label='Ethereum Address'
padding={{ top: 'spacing5', bottom: 'spacing5' }}
{...mergeProps(form.getFieldProps(BOB_RECIPIENT_FIELD, false, true))}
/>
</Flex>
<Flex direction='column' gap='spacing4'>
<AuthCTA type='submit' size='large'>
Claim your NFT
</AuthCTA>
</Flex>
</Flex>
</form>
</>
) : // eslint-disable-next-line no-negated-condition
!selectedAccount ? (
<Flex direction='column' gap='spacing4'>
<P align='center'>Please connect your wallet</P>
</Flex>
) : (
<Flex direction='column' gap='spacing4'>
<P align='center'>Sorry, this account is not eligible.</P>
</Flex>
)}
</Flex>
</Card>
</StyledWrapper>
</MainContainer>
);
};

export default withErrorBoundary(BOB, {
FallbackComponent: ErrorFallback,
onReset: () => {
window.location.reload();
}
});
3 changes: 3 additions & 0 deletions src/pages/BOB/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import BOB from './BOB';

export default BOB;
1 change: 1 addition & 0 deletions src/utils/constants/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const URL_PARAMETERS = Object.freeze({
const PAGES = Object.freeze({
HOME: '/',
BTC: '/btc',
BOB: '/bob-x-interlay',
STRATEGIES: '/strategies',
STRATEGY: `/strategies/:${URL_PARAMETERS.STRATEGY.TYPE}`,
SEND_AND_RECEIVE: '/send-and-receive',
Expand Down
Loading

0 comments on commit 8eb69f8

Please sign in to comment.