Skip to content

Commit

Permalink
Merge pull request #7 from peterferguson/feat/coupon-nft
Browse files Browse the repository at this point in the history
Feat/coupon nft
  • Loading branch information
jamesmccomish authored Mar 17, 2024
2 parents 5ba8e82 + 8c3a7dc commit b6bf1bf
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 4 deletions.
35 changes: 35 additions & 0 deletions apps/dapp/src/components/nft-image-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { getNftUri, getBalance } from "@/lib/example-nft";
import React from "react";

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

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

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

return (
<Card>
<CardHeader>
<CardTitle>10 Eth Coupon</CardTitle>
<CardDescription>Claim your free 10 eth coupon which can</CardDescription>
</CardHeader>
<CardContent>
<img src={uri} alt="NFT" />
</CardContent>
<CardFooter>
<p>You have {hasClaimed ? '' : 'not'} claimed</p>
</CardFooter>
</Card>
);
}
79 changes: 79 additions & 0 deletions apps/dapp/src/components/ui/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as React from "react"

import { cn } from "@/lib/utils"

const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"

const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"

const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"

const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"

const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"

const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
66 changes: 66 additions & 0 deletions apps/dapp/src/lib/example-nft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { encodeFunctionData, type Address } from "viem"
import { walletClient, publicClient } from './permissionless'
import { formatExecutionCalldata } from "./safe-account"

/**
* @dev Example NFT contract functions
*/

// addresses & abis --------------------------------------------
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" }
]

// nft functions --------------------------------------------
export const getNftUri = async (tokenId: bigint) => await publicClient.readContract({
address: NFT_ADDRESS,
abi: EXAMPLE_NFT_ABI,
functionName: 'uri',
args: [tokenId],
})

export const getBalance = async (address: Address, tokenId: bigint) => {
if (!address) return 0
const results = await publicClient.readContract(
{
address: NFT_ADDRESS,
abi: EXAMPLE_NFT_ABI,
functionName: 'balanceOf',
args: [address, tokenId],
}
)
return results
}

// nft function formatting --------------------------------------------
/// @dev To be called from the Onit 4337 account, we encode our function call into an executeUserOp payload
export const formatMintCouponToModuleExecutionCalldata = () => formatExecutionCalldata(
NFT_ADDRESS,
0n,
encodeFunctionData({
abi: EXAMPLE_NFT_ABI,
functionName: 'mintCoupon',
args: [],
}),
0
)

/// @dev To be called from the Onit 4337 account, we encode our function call into an executeUserOp payload
export const formatExchangeForNftToModuleExecutionCalldata = () => formatExecutionCalldata(
NFT_ADDRESS,
0n,
encodeFunctionData({
abi: EXAMPLE_NFT_ABI,
functionName: 'exchangeForNft',
args: [],
}),
0
)



41 changes: 41 additions & 0 deletions apps/dapp/src/lib/safe-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { encodeFunctionData, type Address, type Hex } from "viem"
import { walletClient, publicClient } from './permissionless'

/**
* @dev Onit Safe 4337 Module functions
*/

// abis --------------------------------------------
const ONIT_FACTORY_ADDRESS = '0x42ab880ea77fc7a09eb6ba0fe82fbc9901c114b6'

const ONIT_FACTORY_ABI = [
{ "type": "function", "name": "createSafe4337", "inputs": [{ "name": "passkeyPublicKey", "type": "uint256[2]", "internalType": "uint256[2]" }, { "name": "nonce", "type": "uint256", "internalType": "uint256" }], "outputs": [{ "name": "onitAccountAddress", "type": "address", "internalType": "address" }], "stateMutability": "nonpayable" }
]
const ONIT_SAFE_4337_ABI = [
{ "type": "function", "name": "executeUserOp", "inputs": [{ "name": "to", "type": "address", "internalType": "address" }, { "name": "value", "type": "uint256", "internalType": "uint256" }, { "name": "data", "type": "bytes", "internalType": "bytes" }, { "name": "operation", "type": "uint8", "internalType": "uint8" }], "outputs": [], "stateMutability": "nonpayable" }
]

// safe factory calls --------------------------------------------
export const createSafe4337Account = async () => {
const { request } = await publicClient.simulateContract({
account: walletClient.account,
address: ONIT_FACTORY_ADDRESS,
abi: ONIT_FACTORY_ABI,
functionName: 'createSafe4337',
args: [[1n, 2n], 3n],
})
console.log({ request })

const deployResponse = await walletClient.writeContract(request)
console.log({ deployResponse })

return deployResponse
}

// 4337 module calls --------------------------------------------
/// @dev Build payload which the entryPoint will call on the sender Onit 4337 account
export const formatExecutionCalldata = (to: Address, value: bigint, data: Hex, operation: number) => encodeFunctionData({
abi: ONIT_SAFE_4337_ABI,
functionName: 'executeUserOp',
args: [to, value, data, operation],
})
18 changes: 15 additions & 3 deletions apps/dapp/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
import "../../public/global.css";
import { CreatePaymentButton } from "@/components/create-payment-button";
import { NftImageCard } from "@/components/nft-image-card";
import { getPaymentOrigin } from "@/lib/utils";
const paymentOrigin = getPaymentOrigin();
Expand All @@ -21,11 +23,21 @@ import WalletIframeDialog from "@/components/wallet-iframe-dialog.astro";
<h1>SPC dApp</h1>

<div id="iframe-container" class="hidden">
<iframe name="iframe" allow={`payment ${paymentOrigin}`}></iframe>
<iframe name="iframe" allow={`payment ${paymentOrigin}`}
></iframe>
</div>

<WalletIframeDialog />
<CreatePaymentButton client:only />
<main
class="flex flex-row items-center justify-between min-h-[20vh] bg-red-300"
>
<main
class="flex flex-col items-center justify-evenly min-h-[20vh] bg-green-300"
>
<WalletIframeDialog />
<CreatePaymentButton client:only />
</main>
<NftImageCard client:only />
</main>
</main>
<script>
import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants";
Expand Down
3 changes: 2 additions & 1 deletion packages/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ https://book.getfoundry.sh/
| Contract | Address | Description |
| :-------------------- | :---------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------- |
| Add Modules Lib | [0x58e912c126f92ccd3c6856a0d1104a30d5260e2b](https://sepolia.basescan.org/address/0x58e912c126f92ccd3c6856a0d1104a30d5260e2b#code) | Utility lib for deploying a Safe with enabled modules |
| Onit Account Factory | [0x5c2f5064510eddc536d07129d9bacb4cab5276a2](https://sepolia.basescan.org/address/0x5c2f5064510eddc536d07129d9bacb4cab5276a2#code) | Factory for ERC4337 Module controlled Safes |
| Onit Account Factory | [0x5c2f5064510eddc536d07129d9bacb4cab5276a2](https://sepolia.basescan.org/address/0x5c2f5064510eddc536d07129d9bacb4cab5276a2#code) | Factory for ERC4337 Module controlled Safes
| Example NFT | [0x4A56fD1D63D99978FDb3aC5C152ea122514b6792](https://sepolia.basescan.org/address/0x4A56fD1D63D99978FDb3aC5C152ea122514b6792#code) | Simple NFT used in tests & demo |
|

## Usage
Expand Down
22 changes: 22 additions & 0 deletions packages/contracts/script/deploy/ExampleNftDeployer.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console2} from "forge-std/Script.sol";

import {ExampleNft} from "../../src/dapp-examples/ExampleNft.sol";

contract ExampleNftDeployer is Script {
string public constant EXAMPLE_NFT_URI_1 =
"https://res.cloudinary.com/merkle-manufactory/image/fetch/c_fill,f_jpg,w_168/https%3A%2F%2Fi.imgur.com%2F7Q0QBrm.jpg";
string public constant EXAMPLE_NFT_URI_2 =
"https://res.cloudinary.com/merkle-manufactory/image/fetch/c_fill,f_jpg,w_168/https%3A%2F%2Fi.imgur.com%2F4t3zVHj.jpg";

function setUp() public {}

function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
ExampleNft exampleNft = new ExampleNft(EXAMPLE_NFT_URI_1, EXAMPLE_NFT_URI_2);
console2.log("implementation", address(exampleNft));
}
}
44 changes: 44 additions & 0 deletions packages/contracts/src/dapp-examples/ExampleNft.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import {ERC1155} from "solady/tokens/ERC1155.sol";

/**
* @title ExampleNft
* @dev This contract is an example of an ERC1155 NFT contract.
* It is used to demonstrate the usage of the Safe4337Module and it not meant to be used in production!
*/
contract ExampleNft is ERC1155 {
/// @dev Constants for the token types, 0 for dapp token, 1 for NFT
uint256 public constant DAPP_TOKEN = 0;
uint256 public constant NFT = 1;

mapping(uint256 => string) public uris;

constructor(string memory uri1, string memory uri2) ERC1155() {
// Set some URIs for the tokens
uris[DAPP_TOKEN] = uri1;
uris[NFT] = uri2;
}

function uri(uint256 id) public view override returns (string memory) {
return uris[id];
}

/**
* @dev This function is used to demonstrate the usage of the Safe4337Module
* It mints 10 ether of token 0 to the caller
*/
function mintCoupon() public {
_mint(msg.sender, DAPP_TOKEN, 10 ether, "");
}

/**
* @dev This function is used to demonstrate the usage of the Safe4337Module
* It burns 1 ether of token 0 from the caller, in exchange for an NFT
*/
function exchangeForNft() public {
_burn(msg.sender, DAPP_TOKEN, 1 ether);
_mint(msg.sender, NFT, 1, "");
}
}

0 comments on commit b6bf1bf

Please sign in to comment.