Skip to content

Commit

Permalink
Merge pull request #8 from peterferguson/feat/mint-nft-from-spc
Browse files Browse the repository at this point in the history
Feat/mint nft from spc
  • Loading branch information
peterferguson authored Mar 17, 2024
2 parents 8ae4a40 + c9631af commit 7a90d81
Show file tree
Hide file tree
Showing 27 changed files with 635 additions and 373 deletions.
2 changes: 2 additions & 0 deletions apps/dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
"permissionless": "^0.1.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"spc-lib": "workspace:*",
"tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.4.2",
"usehooks-ts": "^3.0.1",
"viem": "^2.8.10"
}
}
124 changes: 124 additions & 0 deletions apps/dapp/src/components/claim-coupon-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Button } from "@/components/ui/button";
import { formatMintCouponToModuleExecutionCalldata } from "@/lib/example-nft";
import { bundlerClient, publicClient } from "@/lib/permissionless";
import { getPayeeOrigin, getPaymentOrigin } from "@/lib/utils";
import {
ENTRYPOINT_ADDRESS_V07,
getAccountNonce,
getUserOperationHash,
type UserOperation,
} from "permissionless";
import {
fallbackToIframeCredentialCreation,
getAvailableCredentials,
getPaymentDetails,
payWithSPC,
} from "spc-lib";
import type { Address } from "viem";
import { baseSepolia } from "viem/chains";

export function ClaimCouponButton() {
return (
<Button
id="create-payment-button"
onClick={async (e) => {
e.preventDefault();

// - allow only a single credential for now
const [credential] =
(await getAvailableCredentials(getPaymentOrigin())) ?? [];

const address = credential?.address as Address | undefined;
if (!address) {
return fallbackToIframeCredentialCreation();
}

const { userOperation, userOperationHash } =
await getMintNftUserOperation(address);

const {
response: { signature },
} = await payWithSPC(
{
rpId: getPaymentOrigin().replace("https://", ""),
allowedCredentials: [credential.credentialId],
challenge: userOperationHash,
timeout: 60000,
},
getPayeeOrigin(),
getPaymentDetails("0.00"),
address,
);

await bundlerClient.sendUserOperation({
userOperation: {
...userOperation,
signature: `0x${signature}`,
},
});
}}
>
Claim Coupon
</Button>
);
}

const getMintNftUserOperation = async (address: Address) => {
const nonce = await getAccountNonce(publicClient, {
sender: address,
entryPoint: ENTRYPOINT_ADDRESS_V07,
});

console.log("nonce ", nonce);

let userOperation: UserOperation<"v0.7"> = {
sender: address,
nonce,
callData: formatMintCouponToModuleExecutionCalldata(),
// accountGasLimits: 8000000n,
callGasLimit: 900080n,
verificationGasLimit: 8005650n,
preVerificationGas: 801330n,

maxFeePerGas: 113000000n,
maxPriorityFeePerGas: 113000100n,
signature: "0x",
};

// const gasEstimate = await bundlerClient.estimateUserOperationGas({
// userOperation,
// });

// console.log("gasEstimate ", gasEstimate);

// const paymasterData = await paymasterClient.sponsorUserOperation({
// userOperation,
// });

// console.log("paymasterData", paymasterData);

// userOperation = { ...userOperation, ...paymasterData };

// const userOperation = {
// callData:
// "0x7bb374280000000000000000000000004a56fd1d63d99978fdb3ac5c152ea122514b6792000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000443efc46400000000000000000000000000000000000000000000000000000000",
// initCode: "0x",
// callGasLimit: "117688",
// verificationGasLimit: "61421",
// preVerificationGas: "285339",
// maxFeePerGas: "75550508",
// maxPriorityFeePerGas: "110000",
// nonce: "0",
// paymasterAndData: "0x",
// sender: "0x2fe7892721d8279cec0f8687c4b2e922ca7b76b0",
// signature: "0x",
// };

const userOperationHash = getUserOperationHash({
userOperation,
chainId: baseSepolia.id,
entryPoint: ENTRYPOINT_ADDRESS_V07,
});

return { userOperation, userOperationHash };
};
126 changes: 0 additions & 126 deletions apps/dapp/src/components/create-payment-button.tsx

This file was deleted.

40 changes: 27 additions & 13 deletions apps/dapp/src/components/nft-image-card.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,48 @@
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { getNftUri, getBalance } from "@/lib/example-nft";
import React from "react";
import { ClaimCouponButton } from "./claim-coupon-button";
import { useLocalStorage } from "usehooks-ts";
import type { Address } from "viem";

export function NftImageCard() {
const tokenId = 1n;
const [uri, setUri] = React.useState<string | null>(null);
const [hasClaimed, setHasClaimed] = React.useState(false);
const [address] = useLocalStorage<Address | undefined>("address", undefined);

React.useEffect(() => {
getNftUri(0).then((r) => setUri(r));
getNftUri(0n).then((r) => setUri(r));
}, []);

React.useEffect(() => {
getBalance('0x3896a2938d345d3A351cE152AF1b8Cb17bb006be', 0n).then((r) => {
console.log({ r });
setHasClaimed(r > 0)
});
}, []);
getBalance(address, 0n).then((r) => setHasClaimed(r > 0));
}, [address]);

return (
<Card>
<CardHeader>
<CardTitle>10 Eth Coupon</CardTitle>
<CardDescription>Claim your free 10 eth coupon which can</CardDescription>
<CardDescription>
Claim your free 10 eth coupon which can
</CardDescription>
</CardHeader>
<CardContent>
<img src={uri} alt="NFT" />
<CardContent className="flex justify-center min-h-[168px]">
{uri && <img src={uri} alt="NFT" />}
</CardContent>
<CardFooter>
<p>You have {hasClaimed ? '' : 'not'} claimed</p>
<CardFooter className="flex justify-center">
{hasClaimed ? (
<p className="text-center">You have claimed</p>
// TODO: add the txHash of the claim
) : (
<ClaimCouponButton />
)}
</CardFooter>
</Card>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/dapp/src/components/wallet-iframe-dialog.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
import { cn } from "@/lib/utils";
import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants";
import { WALLET_IFRAME_DIALOG_ID } from "spc-lib";
import { getPaymentOrigin } from "@/lib/utils";
const paymentOrigin = getPaymentOrigin();
Expand All @@ -21,7 +21,7 @@ const paymentOrigin = getPaymentOrigin();
allow={`payment ${paymentOrigin}`}></iframe>
</dialog>
<script>
import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants";
import { WALLET_IFRAME_DIALOG_ID } from "spc-lib";
import { getPaymentOrigin } from "@/lib/utils";
const iframeDialog = document.getElementById(
WALLET_IFRAME_DIALOG_ID,
Expand Down
14 changes: 7 additions & 7 deletions apps/dapp/src/lib/example-nft.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { encodeFunctionData, type Address } from "viem"
import { walletClient, publicClient } from './permissionless'
import { publicClient } from './permissionless'
import { formatExecutionCalldata } from "./safe-account"

/**
Expand All @@ -10,11 +10,11 @@ import { formatExecutionCalldata } from "./safe-account"
const NFT_ADDRESS = '0x4A56fD1D63D99978FDb3aC5C152ea122514b6792'

const EXAMPLE_NFT_ABI = [
{ "inputs": [], "name": "exchangeForNft", "outputs": [], "stateMutability": "nonpayable", "type": "function" },
{ "inputs": [], "name": "mintCoupon", "outputs": [], "stateMutability": "nonpayable", "type": "function" },
{ "inputs": [{ "internalType": "uint256", "name": "id", "type": "uint256" }], "name": "uri", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" },
{ "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "uint256", "name": "id", "type": "uint256" }], "name": "balanceOf", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }
]
{ inputs: [], name: "exchangeForNft", outputs: [], stateMutability: "nonpayable", type: "function" },
{ inputs: [], name: "mintCoupon", outputs: [], stateMutability: "nonpayable", type: "function" },
{ inputs: [{ internalType: "uint256", name: "id", type: "uint256" }], name: "uri", outputs: [{ internalType: "string", name: "", type: "string" }], stateMutability: "view", type: "function" },
{ inputs: [{ internalType: "address", name: "owner", type: "address" }, { internalType: "uint256", name: "id", type: "uint256" }], name: "balanceOf", outputs: [{ internalType: "uint256", name: "", type: "uint256" }], stateMutability: "view", type: "function" }
] as const

// nft functions --------------------------------------------
export const getNftUri = async (tokenId: bigint) => await publicClient.readContract({
Expand All @@ -24,7 +24,7 @@ export const getNftUri = async (tokenId: bigint) => await publicClient.readContr
args: [tokenId],
})

export const getBalance = async (address: Address, tokenId: bigint) => {
export const getBalance = async (address: Address | undefined, tokenId: bigint) => {
if (!address) return 0
const results = await publicClient.readContract(
{
Expand Down
Loading

0 comments on commit 7a90d81

Please sign in to comment.