Skip to content

Commit a8553d0

Browse files
Merge pull request #36 from depatchedmode/v0.12.0
V0.12.0
2 parents 9cd8ceb + 80f1d25 commit a8553d0

File tree

15 files changed

+251
-226
lines changed

15 files changed

+251
-226
lines changed

api/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@ export default async (req) => {
1010
hubHttpUrl: process.env.FARCASTER_HUB
1111
}) : {} as FrameActionDataParsed;
1212

13-
const frameContext = {
14-
searchParams: requestURL.searchParams,
15-
requestURL: payload?.untrustedData.url,
16-
}
13+
const prevFrameName = requestURL.searchParams?.get('currFrame')
1714

18-
return await processFrameRequest(frameContext, frameMessage);
15+
return await processFrameRequest(prevFrameName, frameMessage);
1916
} catch (error) {
2017
console.error(`Error processing request: ${error}`);
2118
}

api/redirect.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

api/txdata.ts

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,112 @@
1-
import { encodeFunctionData } from 'viem';
1+
import { encodeAbiParameters, encodeFunctionData } from 'viem';
2+
import { parseRequest } from '../modules/utils.js';
3+
import { FrameActionDataParsed, getFrameMessage } from 'frames.js';
24

3-
export default async () => {
5+
export default async (req) => {
46
try {
7+
const payload = await parseRequest(req);
8+
const frameMessage = payload ? await getFrameMessage(payload, {
9+
hubHttpUrl: process.env.FARCASTER_HUB
10+
}) : {} as FrameActionDataParsed;
11+
512
// Contract details
6-
const CONTRACT_ADDRESS = '0x8CA328F83387519Eb8B18Ea23fc01bBe92dE2Adc'; // Counter.sol on Base
7-
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'}];
13+
const CONTRACT_ADDRESS = '0xc6d4848c9f01d649dfba170c65a964940a93dca5';
14+
const mintFee = 777000000000000;
15+
16+
const partialZora1155ABI = [
17+
{
18+
'inputs': [],
19+
'name': 'mintFee',
20+
'outputs': [
21+
{
22+
'internalType': 'uint256',
23+
'name': '',
24+
'type': 'uint256'
25+
}
26+
],
27+
'stateMutability': 'view',
28+
'type': 'function'
29+
},
30+
{
31+
'inputs': [
32+
{
33+
'internalType': 'contract IMinter1155',
34+
'name': 'minter',
35+
'type': 'address'
36+
},
37+
{
38+
'internalType': 'uint256',
39+
'name': 'tokenId',
40+
'type': 'uint256'
41+
},
42+
{
43+
'internalType': 'uint256',
44+
'name': 'quantity',
45+
'type': 'uint256'
46+
},
47+
{
48+
'internalType': 'bytes',
49+
'name': 'minterArguments',
50+
'type': 'bytes'
51+
},
52+
{
53+
'internalType': 'address',
54+
'name': 'mintReferral',
55+
'type': 'address'
56+
}
57+
],
58+
'name': 'mintWithRewards',
59+
'outputs': [],
60+
'stateMutability': 'payable',
61+
'type': 'function'
62+
},
63+
];
64+
65+
const minterAddress = '0x04e2516a2c207e84a1839755675dfd8ef6302f0a';
66+
const mintReferral = '0x76963eE4C482fA4F9E125ea3C9Cc2Ea81fe8e8C6';
67+
68+
const tokenId = 2;
69+
const quantity = frameMessage.state ? JSON.parse(frameMessage.state).mintQuantity : 1;
70+
const mintToAddress = frameMessage.connectedAddress;
71+
const comment = frameMessage.inputText || '';
72+
73+
let minterArguments;
74+
if (comment.length > 0) {
75+
minterArguments = encodeAbiParameters(
76+
[
77+
{ name: 'addressArg', type: 'address' },
78+
{ name: 'commentArg', type: 'string' }
79+
],
80+
[mintToAddress as `0x${string}`, comment]
81+
);
82+
} else {
83+
minterArguments = encodeAbiParameters(
84+
[
85+
{ name: 'addressArg', type: 'address' }
86+
],
87+
[mintToAddress as `0x${string}`]
88+
);
89+
}
890

991
// Encode the transaction data for the incrementCount function
1092
const calldata = encodeFunctionData({
11-
abi: abi,
12-
functionName: 'incrementCount',
13-
args: []
93+
abi: partialZora1155ABI,
94+
functionName: 'mintWithRewards',
95+
args: [minterAddress, tokenId, quantity, minterArguments, mintReferral]
1496
});
1597

1698
const txJson = JSON.stringify(
1799
{
18-
chainId: 'eip155:8453', // base
100+
chainId: 'eip155:7777777', // zora
19101
method: 'eth_sendTransaction',
20102
params: {
21-
abi: abi, // JSON ABI of the function selector and any errors
103+
abi: partialZora1155ABI,
22104
to: CONTRACT_ADDRESS,
23105
data: calldata,
24-
value: '0',
106+
value: (mintFee * quantity).toString(),
25107
},
26108
}
27-
)
109+
);
28110

29111
// Return the transaction details to the client for signing
30112
return new Response(

modules/processFrameRequest.ts

Lines changed: 36 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,66 @@ import { html } from "satori-html";
44
import fonts from "../src/fonts.js";
55
import mainLayout from "../src/layouts/main.js";
66
import frames from '../src/frames/index.js';
7-
import { Frame, FrameActionDataParsed, GetFrameHtmlOptions, getFrameHtml } from "frames.js";
7+
import { type Frame, type FrameActionDataParsed, type GetFrameHtmlOptions, getFrameHtml } from 'frames.js';
88
import landingPage from '../src/landing-page.js';
99
import { isFrameStolen } from './antitheft.js';
1010

11+
const DEFAULT_FRAME = 'poster';
12+
const DEFAULT_STATE = {
13+
frame: DEFAULT_FRAME,
14+
};
15+
1116
/**
1217
* Determines the next frame to display based on the context and message of the current frame.
1318
*
14-
* @param frameContext Contains context information about the current frame, such as the source frame.
15-
* @param frameMessage An object containing the parsed data for the frame action.
19+
* @param prevFrameName The name of the frame
20+
* @param frameData An object containing the parsed data for the frame action.
1621
* @returns A promise that resolves to the response for displaying the next frame.
1722
*/
18-
export default async (frameContext, frameMessage: FrameActionDataParsed) => {
19-
let nextFrameName = 'poster';
20-
const prevFrame = frames[frameContext.searchParams?.get('frame')];
23+
export default async (prevFrameName: string, frameData: FrameActionDataParsed) => {
24+
const prevFrame = prevFrameName ? frames[prevFrameName] : null;
2125

26+
let nextState = DEFAULT_STATE;
2227
if (prevFrame && typeof prevFrame.handleInteraction === 'function') {
23-
nextFrameName = await prevFrame.handleInteraction(frameMessage, frameContext);
28+
nextState = await prevFrame.handleInteraction(frameData);
2429
}
2530

26-
if (await isFrameStolen(frameMessage)) {
27-
nextFrameName = 'stolen';
31+
if (await isFrameStolen(frameData)) {
32+
nextState.frame = 'stolen';
2833
}
2934

30-
const nextFrame = await frames[nextFrameName].render(frameMessage);
35+
const nextFrame = frames[nextState.frame];
36+
frameData.state = nextFrame.state = JSON.stringify({ ...nextFrame.state, ...nextState });
3137

3238
// TODO: not yet handling redirects
3339
if (nextFrame) {
34-
return await respondWithFrame(nextFrameName, nextFrame, frameMessage);
40+
return await respondWithFrame(nextFrame, frameData);
3541
} else {
36-
console.error(`Unknown frame requested: ${nextFrameName}`);
42+
console.error(`Unknown frame requested: ${nextState.frame}`);
3743
}
3844
}
3945

40-
// const respondWithRedirect = (redirectURL) => {
41-
// const internalRedirectURL = new URL(`${process.env.URL}/redirect`)
42-
// internalRedirectURL.searchParams.set('redirectURL',redirectURL);
43-
// return new Response('<div>redirect</div>',
44-
// {
45-
// status: 302,
46-
// headers: {
47-
// 'Location': internalRedirectURL.toString(),
48-
// },
49-
// }
50-
// );
51-
// }
52-
5346
/**
5447
* Constructs and responds with the HTML for a given frame based on the simpleFrame object and a frame message.
5548
*
56-
* @param simpleFrame The frame object containing minimal information needed to construct the full frame.
49+
* @param renderedFrame The frame object containing minimal information needed to construct the full frame.
5750
* @param message An object containing the parsed data for the frame action.
5851
* @returns A promise that resolves to a Response object containing the HTML for the frame.
5952
*/
6053
const respondWithFrame = async (
61-
name,
62-
simpleFrame,
63-
message: FrameActionDataParsed
54+
nextFrame,
55+
frameData: FrameActionDataParsed
6456
) => {
6557
const postVars = new URLSearchParams();
66-
postVars.set('frame', name);
67-
const host = process.env.URL;
58+
postVars.set('currFrame', nextFrame.name);
59+
const renderedFrame = await nextFrame.render(frameData);
6860
const frame: Frame = {
6961
version: 'vNext',
70-
image: await handleImageSource(simpleFrame, message),
71-
buttons: simpleFrame.buttons,
72-
inputText: simpleFrame.inputText,
73-
postUrl: `${host}/?${postVars.toString()}`
62+
image: await handleImageSource(renderedFrame.image, frameData),
63+
buttons: renderedFrame.buttons,
64+
inputText: renderedFrame.inputText,
65+
postUrl: `${process.env.URL}/?${postVars.toString()}`,
66+
state: nextFrame.state,
7467
};
7568

7669
const index = await landingPage(frame);
@@ -94,15 +87,13 @@ const respondWithFrame = async (
9487
);
9588
};
9689

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

102-
const { imageURL, imageMarkup } = frame;
103-
104-
if (imageMarkup) {
105-
const frameMarkupInLayout = mainLayout(imageMarkup, message)
95+
if (htmlPattern.test(image)) {
96+
const frameMarkupInLayout = mainLayout(image, frameData)
10697
const svg = await satori(
10798
html(frameMarkupInLayout),
10899
{
@@ -117,20 +108,13 @@ async function handleImageSource(frame, message):Promise<string> {
117108
return `data:image/png;base64,${imageBuffer.toString('base64')}`;
118109
}
119110

120-
// data URI
121-
else if (dataUriPattern.test(imageURL)) {
122-
return imageURL;
111+
// data URI or external url
112+
else if (dataUriPattern.test(image) || absoluteUrlPattern.test(image)) {
113+
return image;
123114
}
124115

125-
// external image: need to proxy it
126-
else if (absoluteUrlPattern.test(imageURL)) {
127-
const ogImageResponse = await fetch(imageURL);
128-
const dataURI = await ogImageResponse.text(); // Assuming og-image returns the data URI in the response body
129-
return dataURI;
130-
}
131-
132116
// local image
133117
else {
134-
return `${host}/${imageURL}`;
118+
return `${process.env.URL}/${image}`;
135119
}
136120
}

modules/sanitize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default (text) => {
55
try {
66
const window = new JSDOM('').window;
77
const purify = DOMPurify(window);
8-
return purify.sanitize(text);
8+
return purify.sanitize(text).replace(/(<([^>]+)>)/gi, "");
99
} catch {
1010
throw new Error(`That ain't no string mfr`)
1111
}

modules/utils.ts

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import fs from 'fs/promises';
2-
import path from 'path';
31
import { URLSearchParams } from 'url';
42

53
const streamToString = async(stream) => {
@@ -28,72 +26,7 @@ const parseRequest = async(req) => {
2826
return data;
2927
}
3028

31-
const flattenObject = (obj, prefix = '') => {
32-
return Object.keys(obj).reduce((acc, k) => {
33-
const pre = prefix.length ? `${prefix}[${k}]` : k;
34-
if (typeof obj[k] === 'object' && obj[k] !== null) {
35-
Object.assign(acc, flattenObject(obj[k], pre));
36-
} else {
37-
acc[pre] = obj[k];
38-
}
39-
return acc;
40-
}, {});
41-
}
42-
43-
const objectToURLSearchParams = (obj) => {
44-
const flattened = flattenObject(obj);
45-
const params = new URLSearchParams();
46-
for (const key in flattened) {
47-
params.append(key, flattened[key]);
48-
}
49-
return params;
50-
}
51-
52-
const URLSearchParamsToObject = (searchParams) => {
53-
const obj = {};
54-
55-
for (const [key, value] of searchParams.entries()) {
56-
// eslint-disable-next-line no-useless-escape
57-
const keys = key.split(/[\[\]]/g).filter(k => k);
58-
let currentObj = obj;
59-
60-
for (let i = 0; i < keys.length - 1; i++) {
61-
const k = keys[i];
62-
if (!currentObj[k]) {
63-
currentObj[k] = isNaN(parseInt(keys[i + 1])) ? {} : [];
64-
}
65-
currentObj = currentObj[k];
66-
}
67-
68-
const lastKey = keys[keys.length - 1];
69-
if (Array.isArray(currentObj)) {
70-
if (isNaN(lastKey)) {
71-
currentObj.push(value);
72-
} else {
73-
currentObj[lastKey] = value;
74-
}
75-
} else {
76-
currentObj[lastKey] = value;
77-
}
78-
}
79-
80-
return obj;
81-
}
82-
83-
const loadFont = async (fileName) => {
84-
try {
85-
const filePath = path.join(__dirname, '../public', 'fonts', fileName);
86-
const fontData = await fs.readFile(filePath);
87-
return fontData;
88-
} catch (error) {
89-
console.error('Error reading font file:', error);
90-
}
91-
}
92-
9329
export {
9430
streamToString,
9531
parseRequest,
96-
objectToURLSearchParams,
97-
URLSearchParamsToObject,
98-
loadFont
9932
}

0 commit comments

Comments
 (0)