Skip to content

[WIP] start wearable claim #265

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

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/next/lib/NavLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import React, { useEffect } from 'react';
import { NavBar } from 'app/ui/navbar';
import { useRouter } from 'next/router';
import { BottomTabs } from 'app/ui/navbar/BottomTabs';
import { Box } from 'app/ui/layout/Box';
import { wagmiClient } from 'app/provider/web3/connectKit';

type NavLayoutProps = {
children: React.ReactNode;
Expand All @@ -17,6 +18,10 @@ export const NavLayout: React.FC<NavLayoutProps> = ({ children }) => {
setIsOpen(false);
}, [pathname]);

useEffect(() => {
wagmiClient.autoConnect();
}, []);

const links = [
{
href: '/shop',
Expand Down
2 changes: 1 addition & 1 deletion apps/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@expo/next-adapter": "^4.0.13",
"@mf/api": "*",
"app": "*",
"connectkit-next-siwe": "^0.0.2",
"connectkit-next-siwe": "0.1.0",
"next": "^13.0.6",
"next-auth": "^4.16.4",
"raf": "^3.4.1",
Expand Down
2 changes: 1 addition & 1 deletion apps/next/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Provider } from 'app/provider';
import Head from 'next/head';
import React from 'react';
import React, { useEffect } from 'react';
import type { SolitoAppProps } from 'solito';
import 'raf/polyfill';
import '../global.css';
Expand Down
4 changes: 2 additions & 2 deletions apps/next/pages/api/siwe/[...route].ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import { siwe } from 'shared/auth/siwe';
export default siwe.apiRouteHandler;
import { siweServer } from 'shared/auth/siweServer';
export default siweServer.apiRouteHandler;
10 changes: 5 additions & 5 deletions apps/next/pages/inventory.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NavLayout } from '../lib/NavLayout';
import { NavLayout } from '~/lib/NavLayout';
import type { SolitoPage } from 'solito';
import { SecondScreen } from 'app/features/home/SecondScreen';
import { Inventory } from 'app/features/inventory/Inventory';

const Settings: SolitoPage = () => <SecondScreen title={'My Inventory'} />;
const InventoryPage: SolitoPage = () => <Inventory />;

Settings.getLayout = (page) => <NavLayout>{page}</NavLayout>;
InventoryPage.getLayout = (page) => <NavLayout>{page}</NavLayout>;

export default Settings;
export default InventoryPage;
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@trpc/client": "10.8.2",
"@trpc/server": "10.8.2",
"@wagmi/core": "0.10.8",
"graphql-request": "^5.1.0",
"services": "*",
"shared": "*",
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/root.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createTRPCRouter } from './trpc';
import { productRouter } from './products/router';
import { authRouter } from './auth/router';
import { wearablesRouter } from './wearables/router';

export const appRouter = createTRPCRouter({
product: productRouter,
auth: authRouter,
wearables: wearablesRouter,
});

// export type definition of API
Expand Down
12 changes: 9 additions & 3 deletions packages/api/src/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@
*
*/
import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
import { Chain } from 'wagmi';

import { siwe, SiweSession } from 'shared/auth/siwe';
import { siweServer, SiweSession } from 'shared/auth/siweServer';
import { mfosClient } from 'services/mfos/client';
import { hasuraClient } from 'services/graphql/client';
import { ChainsById } from 'shared/config/chains';

interface CreateInnerContextOptions {
session: SiweSession | null;
chain: Chain;
}
/**
* This helper generates the "internals" for a tRPC context. If you need to use
Expand All @@ -36,7 +40,9 @@ interface CreateInnerContextOptions {
const createInnerTRPCContext = (opts: CreateInnerContextOptions) => {
return {
session: opts.session,
chain: opts.chain,
mfosClient,
hasuraClient,
};
};

Expand All @@ -48,11 +54,11 @@ const createInnerTRPCContext = (opts: CreateInnerContextOptions) => {
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;

// Get the session from the server using the unstable_getServerSession wrapper function
const session = await siwe.getSession(req, res);
const session = await siweServer.getSession(req, res);

return createInnerTRPCContext({
session,
chain: ChainsById[session?.chainId || 1],
});
};

Expand Down
7 changes: 7 additions & 0 deletions packages/api/src/utils/wagmiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createClient } from '@wagmi/core';
import { provider, webSocketProvider } from 'shared/config/chains';

export const wagmiCoreClient = createClient({
provider,
webSocketProvider,
});
100 changes: 100 additions & 0 deletions packages/api/src/wearables/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import { z } from 'zod';
import { readContract } from '@wagmi/core';
import { BigNumber } from '@ethersproject/bignumber';

import {
NftWearablesAbi,
NftWearablesAddress,
} from 'contracts/abis/NftWearables';
import { NftClaim } from './types';
import { productNftMetadataSelector } from 'services/mfos/products/selectors';
import { getMetadataForProduct } from 'shared/utils/wearableMetadata';

export const wearablesRouter = createTRPCRouter({
merkleClaims: protectedProcedure.query(async ({ ctx }) => {
const res = await ctx.hasuraClient.query({
robot_merkle_claims: [
{
where: {
merkle_root: { network: { _eq: ctx.chain.network } },
recipient_eth_address: { _eq: ctx.session.address },
},
},
{
claim_json: [{}, true],
merkle_root_hash: true,
},
],
});

return res.robot_merkle_claims as NftClaim[];
// return nftClaimArray.map((nftClaim) => {
// const claim_count = nftClaim.claim_json.erc1155[0].ids.length;
//
// return {
// ...nftClaim,
// claim_json: {
// ...nftClaim.claim_json,
// },
// };
// });
}),
byAddress: publicProcedure
.input(z.string().optional())
.query(async ({ ctx, input }) => {
const address = input || ctx.session?.address;
if (!address) return null;

try {
const tokenIdRes = await ctx.mfosClient('query')({
products: [
{ filter: { nft_token_id: { _nnull: true } } },
{ nft_token_id: true, id: true },
],
});

const allTokenIds = tokenIdRes.products.map((p) =>
// Query filters non nulls
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
BigNumber.from(p.nft_token_id!),
);

const balances = await readContract({
address: NftWearablesAddress[ctx.chain.id],
abi: NftWearablesAbi,
functionName: 'balanceOfBatch',
args: [Array(allTokenIds.length).fill(address), allTokenIds],
chainId: ctx.chain.id,
});
const tokenIdsForUser = balances.reduce(
(ids: number[], balance, index) => {
if (balance.isZero()) return ids;

const tokenId = allTokenIds[index].toNumber();
return [...ids, tokenId];
},
[],
);

if (!tokenIdsForUser.length) return [];

const nftMetadataRes = await ctx.mfosClient('query')({
products: [
{ filter: { nft_token_id: { _in: tokenIdsForUser } } },
productNftMetadataSelector,
],
});

return (nftMetadataRes.products || []).map((p) => ({
id: p.id,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
nft_token_id: p.nft_token_id!,
nft_metadata: getMetadataForProduct(p),
}));
} catch (e) {
console.log(e);
return [];
}
}),
});
34 changes: 34 additions & 0 deletions packages/api/src/wearables/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { HexString } from 'shared/utils/stringHelpers';

export type NftItem = {
nft_token_id: number;
id: number;
nft_metadata: {
name: string;
image: string;
files: { uri: string; mimeType: string }[];
properties: {
brand: string;
images: string[];
};
};
};

export type NftClaim = {
claim_json: {
to: HexString;
erc1155: {
contractAddress: HexString;
ids: string[];
values: number[];
}[];
erc721: never[];
erc20: {
contractAddresses: never[];
amounts: never[];
};
salt: HexString;
proof: HexString[];
};
merkle_root_hash: HexString;
};
44 changes: 44 additions & 0 deletions packages/app/features/inventory/Inventory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { H3 } from 'app/ui/typography';
import { Box } from 'app/ui/layout/Box';
import { api } from 'app/lib/api';
import { WearableCard } from 'app/ui/components/WearableCard';
import { useAccount, useEnsName } from 'wagmi';
import { formatAddress } from 'shared/utils/addressHelpers';

type InventoryProps = {
// address?: string;
};

export const Inventory: React.FC<InventoryProps> = () => {
const { address } = useAccount();
const { data, isLoading } = api.wearables.byAddress.useQuery(address);

const ensNameQuery = useEnsName({
address,
});

return (
<Box className="flex-1 items-center justify-center p-3">
<H3>{`${formatAddress(address, ensNameQuery.data)}'s Inventory`}</H3>
<Box
className={
'grid w-full max-w-screen-lg grid-cols-1 gap-4 md:grid-cols-2'
}
>
{data ? (
data.map((wearable) => {
return (
<WearableCard
key={wearable.id}
metadata={wearable.nft_metadata}
tokenId={wearable.nft_token_id}
/>
);
})
) : (
<H3>{isLoading ? 'Loading' : 'No Wearables'}</H3>
)}
</Box>
</Box>
);
};
73 changes: 73 additions & 0 deletions packages/app/features/inventory/useClaimWearables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import _ from 'lodash';
import {
useContractRead,
usePrepareContractWrite,
useContractWrite,
mainnet,
} from 'wagmi';
import { api } from 'app/lib/api';
import { NftGiveawayAddress, NftGiveawayAbi } from 'contracts/abis/NftGiveaway';
import { BigNumber } from 'ethers';

export const useClaimWearables = ({ address }: { address: `0x${string}` }) => {
const { data: wearableClaims, isLoading: wearableClaimsLoading } =
api.wearables.merkleClaims.useQuery();

const rootHashes =
wearableClaims?.map((nftClaim) => nftClaim.merkle_root_hash) || [];
const { data: claimedStatuses, isLoading: claimStatusLoading } =
useContractRead({
abi: NftGiveawayAbi,
address: NftGiveawayAddress[mainnet.id],
functionName: 'getClaimedStatus',
args: [address, rootHashes],
enabled: Boolean(address && rootHashes.length),
});

const unclaimedWearableClaims = _.reduce(
claimedStatuses as boolean[],
(
unclaimed: NonNullable<typeof wearableClaims>,
currentValue: boolean,
currentIndex: number,
) => {
if (currentValue || !wearableClaims) return unclaimed;

const unclaimedNftClaim = wearableClaims[currentIndex];

if (unclaimedNftClaim) unclaimed.push(unclaimedNftClaim);

return unclaimed;
},
[],
);

const claimsJSON = _.map(wearableClaims, (nftClaim) => ({
...nftClaim.claim_json,
erc1155: nftClaim.claim_json.erc1155.map((nft) => ({
...nft,
// TODO: test if it works without converting to BigNumber
ids: nft.ids.map(BigNumber.from),
values: nft.values.map(BigNumber.from),
})),
}));
const merkleProofs = _.map(
wearableClaims,
(nftClaim) => nftClaim.claim_json.proof,
);

const { config } = usePrepareContractWrite({
address: NftGiveawayAddress[mainnet.id],
abi: NftGiveawayAbi,
functionName: 'claimMultipleTokensFromMultipleMerkleTree',
args: [rootHashes, claimsJSON, merkleProofs],
});

const claimWearablesWrite = useContractWrite(config);

return {
claimWearablesWrite,
unclaimedWearableClaims,
isLoading: claimStatusLoading || wearableClaimsLoading,
};
};
Loading