diff --git a/.changeset/gorgeous-crabs-tie.md b/.changeset/gorgeous-crabs-tie.md new file mode 100644 index 000000000..4a038b244 --- /dev/null +++ b/.changeset/gorgeous-crabs-tie.md @@ -0,0 +1,5 @@ +--- +"@frames.js/debugger": patch +--- + +feat: mint tx via reservoir api diff --git a/packages/debugger/app/mint/route.tsx b/packages/debugger/app/mint/route.tsx new file mode 100644 index 000000000..22ce5a4c5 --- /dev/null +++ b/packages/debugger/app/mint/route.tsx @@ -0,0 +1,130 @@ +import { createClient, reservoirChains } from "@reservoir0x/reservoir-sdk"; +import { TransactionTargetResponse, getTokenFromUrl } from "frames.js"; +import { NextRequest, NextResponse } from "next/server"; +import { + createPublicClient, + createWalletClient, + hexToBigInt, + http, + parseAbi, +} from "viem"; +import * as viemChains from "viem/chains"; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const taker = searchParams.get("taker"); + const target = searchParams.get("target"); // CAIP-10 ID + const referrer = searchParams.get("referrer") || undefined; + + if (!taker || !target) { + throw new Error("Missing required parameters"); + } + + // Extract contract, type, and chain ID from itemId + const { + address: contractAddress, + chainId, + tokenId, + } = getTokenFromUrl(target); + + const reservoirChain = [...Object.values(reservoirChains)].find( + (chain) => chain.id === chainId + ); + + const viemChain = Object.values(viemChains).find( + (chain) => chain.id === chainId + ); + + if (!reservoirChain || !viemChain) { + throw new Error("Unsupported chain"); + } + + const publicClient = createPublicClient({ + chain: viemChain, + transport: http(), + }); + + const ERC1155_ERC165 = "0xd9b67a26"; + const ERC721_ERC165 = "0x80ac58cd"; + + async function supportsInterface( + interfaceId: `0x${string}` + ): Promise { + return await publicClient + .readContract({ + address: contractAddress as `0x${string}`, + abi: parseAbi([ + "function supportsInterface(bytes4 interfaceID) external view returns (bool)", + ]), + functionName: "supportsInterface", + args: [interfaceId], + }) + .catch((err) => { + console.error(err); + return false; + }); + } + + // Get token type + const [isERC721, isERC1155] = await Promise.all([ + supportsInterface(ERC721_ERC165), + supportsInterface(ERC1155_ERC165), + ]); + + let buyTokenPartial: { token?: string; collection?: string }; + if (isERC721) { + buyTokenPartial = { collection: contractAddress }; + } else if (isERC1155) { + buyTokenPartial = { token: `${contractAddress}:${tokenId}` }; + } else { + buyTokenPartial = { collection: contractAddress }; + } + + // Create reservoir client with applicable chain + const reservoirClient = createClient({ + chains: [{ ...reservoirChain, active: true }], + }); + + const wallet = createWalletClient({ + account: taker as `0x${string}`, + transport: http(), + chain: viemChain, + }); + + const res = await reservoirClient.actions.buyToken({ + items: [{ ...buyTokenPartial, quantity: 1, fillType: "mint" }], + options: { + referrer, + }, + wallet, + precheck: true, + onProgress: () => void 0, + }); + + if (res === true) { + return NextResponse.json(res); + } + + const mintTx = res.steps?.find((step) => step?.id === "sale")?.items?.[0]; + + const txResponse: TransactionTargetResponse = { + chainId: `eip155:${viemChain.id}`, + method: "eth_sendTransaction", + params: { + ...mintTx!.data, + value: hexToBigInt(mintTx?.data.value).toString(), + }, + }; + + return NextResponse.json({ + data: txResponse, + explorer: viemChain.blockExplorers?.default, + }); + } catch (err: any) { + return NextResponse.json( + { message: err.response?.data?.message || err.message }, + { status: err.status ?? 400 } + ); + } +} diff --git a/packages/debugger/app/page.tsx b/packages/debugger/app/page.tsx index 61d79d498..d172c9dde 100644 --- a/packages/debugger/app/page.tsx +++ b/packages/debugger/app/page.tsx @@ -116,6 +116,37 @@ export default function App({ signerState, extraButtonRequestPayload: { mockData: mockHubContext }, onTransaction, + onMint(t) { + if (!confirm(`Mint ${t.target}?`)) { + return; + } + + if (!account.address) { + openConnectModal?.(); + return; + } + + const searchParams = new URLSearchParams({ + target: t.target, + taker: account.address, + }); + + fetch(`/mint?${searchParams.toString()}`) + .then(async (res) => { + if (!res.ok) { + const json = await res.json(); + throw new Error(json.message); + } + return await res.json(); + }) + .then((json) => { + onTransaction({ ...t, transactionData: json.data }); + }) + .catch((e) => { + alert(e); + console.error(e); + }); + }, }); return ( diff --git a/packages/debugger/package.json b/packages/debugger/package.json index 04bdf62c8..3e4bd621f 100644 --- a/packages/debugger/package.json +++ b/packages/debugger/package.json @@ -16,6 +16,7 @@ "url": "https://github.com/framesjs/frames.js/tree/main/packages/debugger" }, "dependencies": { + "@farcaster/core": "^0.14.7", "@frames.js/render": "^0.0.2", "@noble/ed25519": "^2.0.0", "@radix-ui/react-accordion": "^1.1.2", @@ -25,6 +26,9 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", + "@rainbow-me/rainbowkit": "^2.0.2", + "@reservoir0x/reservoir-sdk": "^2.0.11", + "@tanstack/react-query": "^5.22.2", "@types/node": "^18.17.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", @@ -35,25 +39,22 @@ "clsx": "^2.1.0", "eslint": "^8.56.0", "eslint-config-next": "^14.1.0", + "frames.js": "^0.11.0", "is-port-reachable": "^4.0.0", "lucide-react": "^0.344.0", + "next": "^14.1.3", "open": "^10.0.3", "postcss": "^8", "qrcode.react": "^3.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "tailwind-merge": "^2.2.1", "tailwindcss": "^3.3.0", "tailwindcss-animate": "^1.0.7", "typescript": "^5.3.3", - "yargs": "^17.7.2", - "@farcaster/core": "^0.14.7", - "@rainbow-me/rainbowkit": "^2.0.2", - "@tanstack/react-query": "^5.22.2", - "frames.js": "^0.11.0", - "next": "^14.1.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", "viem": "^2.7.12", - "wagmi": "^2.5.7" + "wagmi": "^2.5.7", + "yargs": "^17.7.2" }, "engines": { "node": ">=18.17.0" @@ -79,4 +80,4 @@ "root": true, "extends": "next" } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 45f479fd2..cca7fb936 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3037,6 +3037,13 @@ dependencies: web-streams-polyfill "^3.1.1" +"@reservoir0x/reservoir-sdk@^2.0.11": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@reservoir0x/reservoir-sdk/-/reservoir-sdk-2.0.11.tgz#400310406d7137119364f124630b61f1973ff57f" + integrity sha512-ha9UrNKyFNB0OSZ82NROA74gKYPo50syOcqI3DeBbSkTR0auioKykmf3kn9WWcrgbZ2km5EONtJ6uwhXFzxfiw== + dependencies: + axios "^1.6.7" + "@resvg/resvg-wasm@2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz#e01164b9a267c822e1ff797daa2fb91b663ea6f0" @@ -4926,6 +4933,15 @@ axe-core@=4.7.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axios@^1.6.7: + version "1.6.8" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" + integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -7488,6 +7504,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -11984,6 +12005,11 @@ proxy-compare@2.5.1: resolved "https://registry.yarnpkg.com/proxy-compare/-/proxy-compare-2.5.1.tgz#17818e33d1653fbac8c2ec31406bce8a2966f600" integrity sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"