Skip to content

Commit

Permalink
Merge pull request #36 from depatchedmode/v0.12.0
Browse files Browse the repository at this point in the history
V0.12.0
  • Loading branch information
depatchedmode authored Apr 26, 2024
2 parents 9cd8ceb + 80f1d25 commit a8553d0
Show file tree
Hide file tree
Showing 15 changed files with 251 additions and 226 deletions.
7 changes: 2 additions & 5 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ export default async (req) => {
hubHttpUrl: process.env.FARCASTER_HUB
}) : {} as FrameActionDataParsed;

const frameContext = {
searchParams: requestURL.searchParams,
requestURL: payload?.untrustedData.url,
}
const prevFrameName = requestURL.searchParams?.get('currFrame')

return await processFrameRequest(frameContext, frameMessage);
return await processFrameRequest(prevFrameName, frameMessage);
} catch (error) {
console.error(`Error processing request: ${error}`);
}
Expand Down
20 changes: 0 additions & 20 deletions api/redirect.ts

This file was deleted.

104 changes: 93 additions & 11 deletions api/txdata.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,112 @@
import { encodeFunctionData } from 'viem';
import { encodeAbiParameters, encodeFunctionData } from 'viem';
import { parseRequest } from '../modules/utils.js';
import { FrameActionDataParsed, getFrameMessage } from 'frames.js';

export default async () => {
export default async (req) => {
try {
const payload = await parseRequest(req);
const frameMessage = payload ? await getFrameMessage(payload, {
hubHttpUrl: process.env.FARCASTER_HUB
}) : {} as FrameActionDataParsed;

// Contract details
const CONTRACT_ADDRESS = '0x8CA328F83387519Eb8B18Ea23fc01bBe92dE2Adc'; // Counter.sol on Base
const abi = [{'inputs':[],'stateMutability':'nonpayable','type':'constructor'},{'inputs':[],'name':'getCurrentCount','outputs':[{'internalType':'uint256','name':'','type':'uint256'}],'stateMutability':'view','type':'function'},{'inputs':[],'name':'incrementCount','outputs':[],'stateMutability':'nonpayable','type':'function'}];
const CONTRACT_ADDRESS = '0xc6d4848c9f01d649dfba170c65a964940a93dca5';
const mintFee = 777000000000000;

const partialZora1155ABI = [
{
'inputs': [],
'name': 'mintFee',
'outputs': [
{
'internalType': 'uint256',
'name': '',
'type': 'uint256'
}
],
'stateMutability': 'view',
'type': 'function'
},
{
'inputs': [
{
'internalType': 'contract IMinter1155',
'name': 'minter',
'type': 'address'
},
{
'internalType': 'uint256',
'name': 'tokenId',
'type': 'uint256'
},
{
'internalType': 'uint256',
'name': 'quantity',
'type': 'uint256'
},
{
'internalType': 'bytes',
'name': 'minterArguments',
'type': 'bytes'
},
{
'internalType': 'address',
'name': 'mintReferral',
'type': 'address'
}
],
'name': 'mintWithRewards',
'outputs': [],
'stateMutability': 'payable',
'type': 'function'
},
];

const minterAddress = '0x04e2516a2c207e84a1839755675dfd8ef6302f0a';
const mintReferral = '0x76963eE4C482fA4F9E125ea3C9Cc2Ea81fe8e8C6';

const tokenId = 2;
const quantity = frameMessage.state ? JSON.parse(frameMessage.state).mintQuantity : 1;
const mintToAddress = frameMessage.connectedAddress;
const comment = frameMessage.inputText || '';

let minterArguments;
if (comment.length > 0) {
minterArguments = encodeAbiParameters(
[
{ name: 'addressArg', type: 'address' },
{ name: 'commentArg', type: 'string' }
],
[mintToAddress as `0x${string}`, comment]
);
} else {
minterArguments = encodeAbiParameters(
[
{ name: 'addressArg', type: 'address' }
],
[mintToAddress as `0x${string}`]
);
}

// Encode the transaction data for the incrementCount function
const calldata = encodeFunctionData({
abi: abi,
functionName: 'incrementCount',
args: []
abi: partialZora1155ABI,
functionName: 'mintWithRewards',
args: [minterAddress, tokenId, quantity, minterArguments, mintReferral]
});

const txJson = JSON.stringify(
{
chainId: 'eip155:8453', // base
chainId: 'eip155:7777777', // zora
method: 'eth_sendTransaction',
params: {
abi: abi, // JSON ABI of the function selector and any errors
abi: partialZora1155ABI,
to: CONTRACT_ADDRESS,
data: calldata,
value: '0',
value: (mintFee * quantity).toString(),
},
}
)
);

// Return the transaction details to the client for signing
return new Response(
Expand Down
88 changes: 36 additions & 52 deletions modules/processFrameRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,66 @@ import { html } from "satori-html";
import fonts from "../src/fonts.js";
import mainLayout from "../src/layouts/main.js";
import frames from '../src/frames/index.js';
import { Frame, FrameActionDataParsed, GetFrameHtmlOptions, getFrameHtml } from "frames.js";
import { type Frame, type FrameActionDataParsed, type GetFrameHtmlOptions, getFrameHtml } from 'frames.js';
import landingPage from '../src/landing-page.js';
import { isFrameStolen } from './antitheft.js';

const DEFAULT_FRAME = 'poster';
const DEFAULT_STATE = {
frame: DEFAULT_FRAME,
};

/**
* Determines the next frame to display based on the context and message of the current frame.
*
* @param frameContext Contains context information about the current frame, such as the source frame.
* @param frameMessage An object containing the parsed data for the frame action.
* @param prevFrameName The name of the frame
* @param frameData An object containing the parsed data for the frame action.
* @returns A promise that resolves to the response for displaying the next frame.
*/
export default async (frameContext, frameMessage: FrameActionDataParsed) => {
let nextFrameName = 'poster';
const prevFrame = frames[frameContext.searchParams?.get('frame')];
export default async (prevFrameName: string, frameData: FrameActionDataParsed) => {
const prevFrame = prevFrameName ? frames[prevFrameName] : null;

let nextState = DEFAULT_STATE;
if (prevFrame && typeof prevFrame.handleInteraction === 'function') {
nextFrameName = await prevFrame.handleInteraction(frameMessage, frameContext);
nextState = await prevFrame.handleInteraction(frameData);
}

if (await isFrameStolen(frameMessage)) {
nextFrameName = 'stolen';
if (await isFrameStolen(frameData)) {
nextState.frame = 'stolen';
}

const nextFrame = await frames[nextFrameName].render(frameMessage);
const nextFrame = frames[nextState.frame];
frameData.state = nextFrame.state = JSON.stringify({ ...nextFrame.state, ...nextState });

// TODO: not yet handling redirects
if (nextFrame) {
return await respondWithFrame(nextFrameName, nextFrame, frameMessage);
return await respondWithFrame(nextFrame, frameData);
} else {
console.error(`Unknown frame requested: ${nextFrameName}`);
console.error(`Unknown frame requested: ${nextState.frame}`);
}
}

// const respondWithRedirect = (redirectURL) => {
// const internalRedirectURL = new URL(`${process.env.URL}/redirect`)
// internalRedirectURL.searchParams.set('redirectURL',redirectURL);
// return new Response('<div>redirect</div>',
// {
// status: 302,
// headers: {
// 'Location': internalRedirectURL.toString(),
// },
// }
// );
// }

/**
* Constructs and responds with the HTML for a given frame based on the simpleFrame object and a frame message.
*
* @param simpleFrame The frame object containing minimal information needed to construct the full frame.
* @param renderedFrame The frame object containing minimal information needed to construct the full frame.
* @param message An object containing the parsed data for the frame action.
* @returns A promise that resolves to a Response object containing the HTML for the frame.
*/
const respondWithFrame = async (
name,
simpleFrame,
message: FrameActionDataParsed
nextFrame,
frameData: FrameActionDataParsed
) => {
const postVars = new URLSearchParams();
postVars.set('frame', name);
const host = process.env.URL;
postVars.set('currFrame', nextFrame.name);
const renderedFrame = await nextFrame.render(frameData);
const frame: Frame = {
version: 'vNext',
image: await handleImageSource(simpleFrame, message),
buttons: simpleFrame.buttons,
inputText: simpleFrame.inputText,
postUrl: `${host}/?${postVars.toString()}`
image: await handleImageSource(renderedFrame.image, frameData),
buttons: renderedFrame.buttons,
inputText: renderedFrame.inputText,
postUrl: `${process.env.URL}/?${postVars.toString()}`,
state: nextFrame.state,
};

const index = await landingPage(frame);
Expand All @@ -94,15 +87,13 @@ const respondWithFrame = async (
);
};

async function handleImageSource(frame, message):Promise<string> {
async function handleImageSource(image, frameData):Promise<string> {
const dataUriPattern = /^data:image\/[a-zA-Z]+;base64,/;
const absoluteUrlPattern = /^https?:\/\//;
const host = process.env.URL;
const htmlPattern = /(<([^>]+)>)/gi;

const { imageURL, imageMarkup } = frame;

if (imageMarkup) {
const frameMarkupInLayout = mainLayout(imageMarkup, message)
if (htmlPattern.test(image)) {
const frameMarkupInLayout = mainLayout(image, frameData)
const svg = await satori(
html(frameMarkupInLayout),
{
Expand All @@ -117,20 +108,13 @@ async function handleImageSource(frame, message):Promise<string> {
return `data:image/png;base64,${imageBuffer.toString('base64')}`;
}

// data URI
else if (dataUriPattern.test(imageURL)) {
return imageURL;
// data URI or external url
else if (dataUriPattern.test(image) || absoluteUrlPattern.test(image)) {
return image;
}

// external image: need to proxy it
else if (absoluteUrlPattern.test(imageURL)) {
const ogImageResponse = await fetch(imageURL);
const dataURI = await ogImageResponse.text(); // Assuming og-image returns the data URI in the response body
return dataURI;
}

// local image
else {
return `${host}/${imageURL}`;
return `${process.env.URL}/${image}`;
}
}
2 changes: 1 addition & 1 deletion modules/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default (text) => {
try {
const window = new JSDOM('').window;
const purify = DOMPurify(window);
return purify.sanitize(text);
return purify.sanitize(text).replace(/(<([^>]+)>)/gi, "");
} catch {
throw new Error(`That ain't no string mfr`)
}
Expand Down
67 changes: 0 additions & 67 deletions modules/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import fs from 'fs/promises';
import path from 'path';
import { URLSearchParams } from 'url';

const streamToString = async(stream) => {
Expand Down Expand Up @@ -28,72 +26,7 @@ const parseRequest = async(req) => {
return data;
}

const flattenObject = (obj, prefix = '') => {
return Object.keys(obj).reduce((acc, k) => {
const pre = prefix.length ? `${prefix}[${k}]` : k;
if (typeof obj[k] === 'object' && obj[k] !== null) {
Object.assign(acc, flattenObject(obj[k], pre));
} else {
acc[pre] = obj[k];
}
return acc;
}, {});
}

const objectToURLSearchParams = (obj) => {
const flattened = flattenObject(obj);
const params = new URLSearchParams();
for (const key in flattened) {
params.append(key, flattened[key]);
}
return params;
}

const URLSearchParamsToObject = (searchParams) => {
const obj = {};

for (const [key, value] of searchParams.entries()) {
// eslint-disable-next-line no-useless-escape
const keys = key.split(/[\[\]]/g).filter(k => k);
let currentObj = obj;

for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
if (!currentObj[k]) {
currentObj[k] = isNaN(parseInt(keys[i + 1])) ? {} : [];
}
currentObj = currentObj[k];
}

const lastKey = keys[keys.length - 1];
if (Array.isArray(currentObj)) {
if (isNaN(lastKey)) {
currentObj.push(value);
} else {
currentObj[lastKey] = value;
}
} else {
currentObj[lastKey] = value;
}
}

return obj;
}

const loadFont = async (fileName) => {
try {
const filePath = path.join(__dirname, '../public', 'fonts', fileName);
const fontData = await fs.readFile(filePath);
return fontData;
} catch (error) {
console.error('Error reading font file:', error);
}
}

export {
streamToString,
parseRequest,
objectToURLSearchParams,
URLSearchParamsToObject,
loadFont
}
Loading

0 comments on commit a8553d0

Please sign in to comment.