Skip to content

Instantly share code, notes, and snippets.

@imthatcarlos
Last active January 9, 2024 23:54
Show Gist options
  • Save imthatcarlos/379cb5c4b3b3851dc714800dc222d378 to your computer and use it in GitHub Desktop.
Save imthatcarlos/379cb5c4b3b3851dc714800dc222d378 to your computer and use it in GitHub Desktop.
React hook for MadFi onchain points + redemption (see: https://docs.madfi.xyz/protocol-overview/onchain-points-and-redemption)
import { useState, useEffect } from 'react';
import { TransactionReceipt } from 'viem';
import { useAccount, useWalletClient } from 'wagmi';
import { getPublicClient } from '@wagmi/core';
import request, { gql } from 'graphql-request';
const CHAIN_ID = 137;
const MADFI_SUBGRAPH_URL = "https://api.thegraph.com/subgraphs/name/mad-finance/madfi-subgraph";
const SBT_REDEMPTION_CONTRACT_ADDRESS = ""; // see: https://docs.madfi.xyz
const SBT_REDEMPTION_ABI = [
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "collectionId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "provider",
"type": "string"
},
{
"indexed": false,
"internalType": "uint128",
"name": "units",
"type": "uint128"
}
],
"name": "Redemption",
"type": "event"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getRewardUnitsRedeemable",
"outputs": [
{
"internalType": "uint128",
"name": "",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "string",
"name": "provider",
"type": "string"
},
{
"internalType": "uint128",
"name": "units",
"type": "uint128"
}
],
"internalType": "struct ISBTRedemption.RedemptionParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "signature",
"type": "bytes"
}
],
"name": "redeemRewardUnits",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];
const _buildRedemptionParamsTypedData = (chainId: number, verifyingContract: `0x${string}`) => ({
types: {
RedemptionParams: [
{ name: 'tokenId', type: 'uint256' },
{ name: 'provider', type: 'string' },
{ name: 'units', type: 'uint128' }
],
},
domain: {
name: 'MadFi Redemptions',
version: '1',
chainId,
verifyingContract,
}
});
const getSignedRedemptionParams = async (
walletClient: any,
params: any,
chainId: number,
verifyingContract: `0x${string}`
) => {
const { domain, types } = _buildRedemptionParamsTypedData(chainId, verifyingContract);
const [account] = await walletClient.getAddresses();
return await walletClient.signTypedData({
account,
domain,
types,
primaryType: "RedemptionParams",
message: params
});
};
export default (collectionId?: string, _tokenId?: string) => {
const { address } = useAccount();
const { data: walletClient } = useWalletClient();
const [isRedeeming, setIsRedeeming] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [redeemablePoints, setRedeemablePoints] = useState<BigInt>(BigInt(0));
const [tokenId, setTokenId] = useState<string | undefined>(_tokenId);
const MAD_SBT_TOKEN_OWNED = gql`
query ($collectionId: String!, $addressLowerCase: String!) {
madSbtTokens(where:{collection_:{collectionId:$collectionId}, owner_:{id:$addressLowerCase}}){
tokenId
rewardPoints
}
}
`;
const fetchTokenId = async (): Promise<string | undefined> => {
if (!(collectionId && address)) return;
// @ts-expect-error: unknown response
const { madSbtTokens } = await request({
url: MADFI_SUBGRAPH_URL,
document: MAD_SBT_TOKEN_OWNED,
variables: { collectionId, addressLowerCase: address.toLowerCase() }
});
const tokenId = madSbtTokens?.length ? madSbtTokens[0].tokenId : undefined;
setTokenId(tokenId)
return tokenId;
};
const fetchRedeemablePoints = async () => {
let _points = BigInt(0);
const _tokenId = tokenId || await fetchTokenId();
if (_tokenId) {
const publicClient = getPublicClient();
const data = await publicClient.readContract({
address: SBT_REDEMPTION_CONTRACT_ADDRESS!,
abi: SBT_REDEMPTION_ABI,
functionName: "getRewardUnitsRedeemable",
args: [_tokenId],
});
_points = data as bigint;
}
setRedeemablePoints(_points);
setIsLoading(false);
};
/**
* Request a signature from the connected wallet client to send to the SBTRedemption contract for redemption. Only
* token holders _or_ verified addresses can call #redeemRewardUnits
* @param provider The string for the third-party provider fulfilling the redemption (ex: 'https://perk.shop')
* @param units The units to redeem
* @returns tx receipt of the redemption tx, or undefined on failure
*/
const redeemPoints = async (provider: string, units: number): Promise<TransactionReceipt | undefined> => {
if (redeemablePoints === 0n) return;
setIsRedeeming(true);
try {
const publicClient = getPublicClient();
const params = { provider, units: units.toString(), tokenId };
const signature = await getSignedRedemptionParams(
walletClient,
params,
CHAIN_ID,
SBT_REDEMPTION_CONTRACT_ADDRESS
);
const hash = await walletClient!.writeContract({
address: SBT_REDEMPTION_CONTRACT_ADDRESS,
abi: SBT_REDEMPTION_ABI,
functionName: "redeemRewardUnits",
args: [params, signature],
});
console.log(`hash: ${hash}`);
return await publicClient.waitForTransactionReceipt({ hash });
} catch (error) {
console.log(error);
}
setIsRedeeming(false);
};
useEffect(() => {
if ((collectionId && walletClient && address)) {
setIsLoading(true);
fetchRedeemablePoints();
}
}, [collectionId, walletClient, address]);
return {
isLoading,
redeemablePoints,
isRedeeming,
redeemPoints,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment