Avatar

jamiedubs

i move away from the mic to breathe in
21 public vals
Joined March 27, 2023

attempt to generate websites using Glif and then store and publish them via Valtown - it's valtownGeocities! a real mouthful

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import { blob } from "https://esm.town/v/std/blob?v=10";
function isJSON(input: string | null | undefined) {
if (!input || input === null) return false;
try {
JSON.parse(input);
return true;
} catch (e) {
return false;
}
}
function processInputHtml(html: string) {
let output = html;
// strip out backticks which gpt loves generating
output = output.replaceAll("```html", "");
output = output.replaceAll("```", "");
return output;
}
function formatPage(key: string, data: string) {
return `
${data}
<br />
<br />
hosted by
<a href="https://www.val.town/v/jamiedubs/valtownGeocities" target="_blank">
valtownGeocities
</a>
| key: ${key}
`;
}
export const valtownGeocities = async (req: Request) => {
const searchParams = new URL(req.url).searchParams;
const format = searchParams.get("format") ?? "html";
const key = searchParams.get("key") ?? "default";
// if (!key) throw new Error("missing ?key=");
console.log("hello", { format, key });
let data;
const oldData = await blob.getJSON(key);
if (req.method == "GET") {
data = { data: oldData };
} else if (req.method == "POST") {
const rawData = await req.text();
console.log("received rawData...", rawData);
// const newData = rawData;
const unprocessedData = isJSON(rawData) ? JSON.parse(rawData)?.data : rawData;
// const newData = processInputHtml(unprocessedData);
const newData = unprocessedData;
console.log("inputs", { rawData, isJSON: isJSON(rawData), unprocessedData, newData });
if (!newData)
throw new Error(
"missing data. put html in your raw POST payload, or if you are POST'ing JSON data, put it in a field named 'data'",
);
await blob.setJSON(key, newData);
data = { oldData, data: newData };
console.log("set", { key, data });
}
else {
throw new Error("unsupported HTTP method");
}
if (format == "json") {
return Response.json({ data });
} else if (format == "html") {
// return new Response(`<pre>${JSON.stringify(data, null, 2)}</pre>`, { headers: { 'Content-Type': 'text/html' } });
const defaultText = "nothing yet. try POST'ing HTML to this URL :)";
return new Response(formatPage(key, data.data ?? defaultText), { headers: { "Content-Type": "text/html" } });
} else {
throw new Error("unsupported format");
}
};

returns image URL (and only image URL) for a given NFT contract + tokenId. Uses Alchemy's NFT API

to this use val, copy the Web API endpoint and use ?query params to specify the contract address and tokenId you want:

https://jamiedubs-nftimage.web.val.run/?contractAddress=0x3769c5700Da07Fe5b8eee86be97e061F961Ae340&tokenId=666 - FIXME valtown is turning & into "&", you need to fix it. even like this broken

plain text by default. for JSON add &format=json, for an <img> tag use &format=html

for other NFT metadata: https://www.val.town/v/jamiedubs.nftMetadata

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { fetchNftMetadata } from "https://esm.town/v/jamiedubs/nftMetadata";
export const nftImage = async (req: Request) => {
const searchParams = new URL(req.url).searchParams;
const contractAddress = searchParams.get("contractAddress");
const tokenId = searchParams.get("tokenId");
const format = searchParams.get("format")?.toLowerCase() ?? "text";
console.log("nftImage", { contractAddress, tokenId });
const json = await fetchNftMetadata(contractAddress, tokenId);
console.log("nftMetadata response =>", json);
const imageUrl = json["metadata"] && json["metadata"]["image"];
if (format == "json") {
return Response.json({ imageUrl });
}
else if (format == "html") {
return new Response(`<img src="${imageUrl}"/>`, {
headers: {
"Content-Type": "text/html",
},
});
}
else {
return new Response(imageUrl, {
headers: {
"Content-Type": "text/plain",
},
});
}
};

use by copying web API endpoint and appending "?contractAddress=...&tokenId..." - like this

uses Alchemy for indexed NFT data: https://docs.alchemy.com/reference/getnftmetadata

plus it's using my personal API key. don't abuse this or I'll disable it! yeesh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { fetch } from "https://esm.town/v/std/fetch";
import process from "node:process";
export async function fetchNftMetadata(contractAddress: string, tokenId: string) {
const apiKey = process.env.ALCHEMY_API_KEY;
const rpcUrl = `https://eth-mainnet.g.alchemy.com/nft/v2/${apiKey}`;
const url = `${rpcUrl}/getNFTMetadata?contractAddress=${contractAddress}&tokenId=${tokenId}`;
const response = await fetch(url, {
method: "GET",
headers: {
"accept": "application/json",
},
});
console.log("Alchemy response", response);
if (!response) {
return { error: "no response from Alchemy" };
} else if (!response.ok) {
return { error: `failed with status: ${response.statusText}` };
}
else {
const json = await response.json();
console.log("response OK", json);
return json;
}
}
export default async function nftMetadata(req: Request): Promise<Response> {
const searchParams = new URL(req.url).searchParams;
const contractAddress = searchParams.get("contractAddress");
const tokenId = searchParams.get("tokenId");
const format = searchParams.get("format")?.toLowerCase() ?? "text";
if (!contractAddress || !tokenId) {
return Response.json({ error: "you must specify ?contractAddress...&tokenId=..." });
}
const json = await fetchNftMetadata(contractAddress, tokenId);
return Response.json(json);
}

returns the last 100 featured glifs on Glif but with a simplified response shape; I use this in other glifs with the WebFetcher block

use like: https://jamiedubs-glifs.web.val.run/

to fetch info for a single glif, try @jamiedubs/glifJson

#glif #glifs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { fetch } from "https://esm.town/v/std/fetch";
export const glifs = async (id: string) => {
const url = `https://glif.app/api/glifs?featured=1`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
const data = json.map((glif) => {
return { id: glif.id, name: glif.name, description: glif.description };
});
return Response.json(data);
// return text
// const data = json.map((glif) => {
// return `${glif.id} ${glif.name}`;
// }).join("\n");
// console.log("data", data);
// return new Response(data, { headers: { "Content-Type": "text/plain" }});
}
catch (error) {
return Response.json({ error: error.message });
}
};

fetches a simplified version of a Glif response object; I use this in other glifs with the WebFetcher block

use like: https://jamiedubs-glifjson.web.val.run/?id=clgh1vxtu0011mo081dplq3xs

to fetch only the raw glif JSON, specify ?data=1: https://jamiedubs-glifjson.web.val.run/?id=clgh1vxtu0011mo081dplq3xs&data=1

to fetch a list of recently featured glifs try @jamiedubs/glifs

#glif #glifs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { fetch } from "https://esm.town/v/std/fetch";
export const glifJson = async (req: Reqeust) => {
const searchParams = new URL(req.url).searchParams;
const id = searchParams.get("id");
const url = `https://glif.app/api/glifs?id=${id}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
const data = searchParams.get("data") ? json[0]?.data : json[0];
if (data) {
return Response.json(data);
}
else {
throw new Error("bad JSON response");
}
}
catch (error) {
return Response.json({ error: error.message });
}
};

fetch token balances from an Ethereum wallet. uses Alchemy. don't abuse my API key or I'll turn this off

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { alchemyFetch } from "https://esm.town/v/jamiedubs/alchemyClient";
export async function getBalances(address: string) {
const [tokens, eth] = await Promise.all([
alchemyFetch(`getTokenBalances?address=${address}`),
alchemyFetch(`getBalance?address=${address}`),
]);
return { tokens, eth };
}
export default async function ethereumTokenBalances(req: Request): Promise<Response> {
const searchParams = new URL(req.url).searchParams;
const address = searchParams.get("address");
// const token = searchParams.get("token");
const format = searchParams.get("format")?.toLowerCase() ?? "text";
if (!address) {
return Response.json({ error: "you must specify ?address" });
}
const json = await getBalances(address);
return Response.json(json);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// glif API mini-SDK
// make generative magical AI things
//
// set your GLIF_API_TOKEN in your own ENV, or you'll hit rate limits
// https://glif.app/settings/api-tokens
//
// call from your val like:
// import { runGlif } from "https://esm.town/v/jamiedubs/runGlif";
// const json = await runGlif({ id: "cluu91eda000cv8jd675qsrby", inputs: ["hello", "world"] });
import process from "node:process";
export interface RunGlifResponse {
id: string;
inputs?: string[];
error?: string;
output?: string;
outputFull?: object;
}
export async function runGlif({
id,
inputs,
}: {
id: string;
inputs: string[];
}): Promise<RunGlifResponse> {
const body = { id, inputs };
const headers = {
Authorization: `Bearer ${process.env.GLIF_API_TOKEN}`,
};
const res = await fetch(`https://simple-api.glif.app`, {
method: "POST",
body: JSON.stringify(body),
headers,
});
if (res.status !== 200) {
const text = await res.text();
return { error: `${res.status} error from glif API: ${text}` };
}
const json = await res.json();
// TODO parse with zod
if (json?.error) return { error: json.error };
return json;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// Farcaster action that summons @glif bot to roast the cast using AI magic
// Install URL: https://warpcast.com/~/add-cast-action?actionType=post&name=roastCast&icon=flame&postUrl=https%3A%2F%2Fjamiedubs-roastCast.web.val.run
import process from "node:process";
async function fetchCast({ fid, hash }: { fid: string; hash: string }) {
console.log("fetchCast", { fid, hash });
const res = await fetch(
`https://api.neynar.com/v2/farcaster/cast?identifier=${hash}&type=hash`,
{
method: "GET",
headers: {
accept: "application/json",
api_key: "NEYNAR_API_DOCS", // FIXME add my own API key pls
},
},
);
const json = await res.json();
const user = `@${json.cast.author.username} (${json.cast.author.display_name})`;
const message = json.cast.text;
return `${user}: ${message}`;
}
interface RunGlifResponse {
error?: string;
output?: string;
}
async function runGlif({ inputs }: { inputs: string[] }): Promise<RunGlifResponse> {
const id = "cluu91eda000cv8jd675qsrby" as const;
const body = { id, inputs };
const headers = {
Authorization: `Bearer ${process.env.GLIF_API_TOKEN}`,
};
const res = await fetch(`https://simple-api.glif.app`, {
method: "POST",
body: JSON.stringify(body),
headers,
});
if (res.status !== 200) {
const text = await res.text();
return { error: `${res.status} error from glif API: ${text}` };
}
const json = await res.json();
if (json?.error) return { error: json.error };
return json;
}
async function postCast({ text, replyToFid, replyToHash }: { text: string; replyToFid: string; replyToHash: string }) {
if (!process.env.FARCASTER_SIGNER_ID || !process.env.PINATA_JWT) {
throw new Error("missing required ENV vars");
}
const cast = {
signerId: process.env.FARCASTER_SIGNER_ID,
castAddBody: {
text,
parentCastId: {
fid: replyToFid,
hash: replyToHash,
},
},
};
const url = "https://api.pinata.cloud/v3/farcaster/casts";
const options = {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PINATA_JWT}`,
"Content-Type": "application/json",
},
body: JSON.stringify(cast),
};
console.log("postCast fetching...", url, cast);
const res = await fetch(url, options);
let json;
try {
json = await res.json();
} catch (err) {
const { status, statusText, body } = err;
console.error("postCast error", { status, statusText, body });
process.exit(1);
}
console.log(JSON.stringify(json, null, 2));
return json;
}
async function runGlifAndPost({ inputs, cast }: { inputs: string[]; cast: { fid: string; hash: string } }) {
const glifRes = await runGlif({ inputs });
console.log({ glifRes });
const postRes = await postCast({ text: glifRes.output, replyToFid: cast.fid, replyToHash: cast.hash });
console.log({ postRes });
return glifRes;
}
export default async function(req: Request): Promise<Response> {

fetch contents of a specific wikipedia page using ?title=param. will return nothing if page doesn't exactly match. example: https://jamiedubs-wikipediapage.web.val.run/?title=Berlin

for a more search-oriented approach, try https://www.val.town/v/jamiedubs/searchWikipedia

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
export const fetchWikipediaContent = async (req: Request) => {
const url = new URL(req.url);
const title = url.searchParams.get("title");
if (!title) {
return new Response("Title parameter is required", {
status: 400, // Bad Request
headers: { "Content-Type": "text/plain" },
});
}
try {
const apiUrl =
`https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&format=json&origin=*&titles=${
encodeURIComponent(title)
}`;
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const pageId = Object.keys(data.query.pages)[0];
if (pageId === "-1") {
return new Response("No content found for the provided title", {
status: 404, // Not Found
headers: { "Content-Type": "text/plain" },
});
}
const content = data.query.pages[pageId].extract;
return new Response(content, {
status: 200, // OK
headers: { "Content-Type": "text/plain" },
});
} catch (error) {
return new Response(`Error fetching Wikipedia content: ${error.message}`, {
status: 500, // Internal Server Error
headers: { "Content-Type": "text/plain" },
});
}
};

dark greetings cryptoadventurers. This val will print the contents of a given Ethereum wallet's Synthetic Loot, which is procedurally generated from your wallet address. To look at your sLoot in a browser with some fun pixel art, check out timshel's Synthetic Loot Viewer

to use this endpoint, pass ?address=0x... e.g. https://jamiedubs-syntheticloot.web.val.run/?account=0xf296178d553c8ec21a2fbd2c5dda8ca9ac905a00

the default response type is JSON. You can also get a simple list of the loot bag contents using ?format=text. e.g. https://jamiedubs-syntheticloot.web.val.run/?account=0xf296178d553c8ec21a2fbd2c5dda8ca9ac905a00&format=text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import { fetch } from "https://esm.town/v/std/fetch";
import process from "node:process";
import { DOMParser } from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";
// I <3 ether.actor, very convenient for fetching on-chain data
// it's a little slow - Alchemy or another RPC/API provider would be faster
const url = `https://ether.actor/0x869ad3dfb0f9acb9094ba85228008981be6dbdde/tokenURI`;
// example data:
// const account = "0xf296178d553c8ec21a2fbd2c5dda8ca9ac905a00";
// const svg = `<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family: serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="10" y="20"
async function fetchAndParseSvgFromJson(account: string) {
try {
const response = await fetch(`${url}/${account}`);
const dataUri = await response.text();
// Decode the JSON from the base64 encoded dataURI
const base64Json = dataUri.split(",")[1];
const decodedJsonString = atob(base64Json);
const json = JSON.parse(decodedJsonString);
console.log({ json });
// Extract the SVG dataURI from the JSON's `image` field
const svgDataUri = json.image;
const base64Svg = svgDataUri.split(",")[1];
const decodedSvg = atob(base64Svg);
console.log(decodedSvg);
return decodedSvg; // or manipulate as needed
} catch (error) {
console.error("Error fetching or decoding SVG from JSON:", error);
}
}
type SvgTextElement = {
content: string;
x: string;
y: string;
class: string;
};
function parseElementsFromSvg(svgString: string): SvgTextElement[] {
const parser = new DOMParser();
// deno-dom only supports HTML
// https://deno.land/x/deno_dom@v0.1.45
// const doc = parser.parseFromString(svgString, "image/svg+xml");
const doc = parser.parseFromString(svgString, "text/html");
const elements = doc.querySelectorAll("text");
const parsedElements: SvgTextElement[] = [];
elements.forEach((element) => {
const content = element.textContent || "";
const x = element.getAttribute("x") || "";
const y = element.getAttribute("y") || "";
const className = element.getAttribute("class") || "";
parsedElements.push({ content, x, y, class: className });
});
return parsedElements;
}
export default async function(req: Request): Promise<Response> {
const url = new URL(req.url);
console.log(url.searchParams);
const account = url.searchParams.get("account");
if (!account) {
return Response.json({ ok: false, error: "you must specify an Ethereum address via ?account=" });
}
const svg = await fetchAndParseSvgFromJson(account);
const elements = parseElementsFromSvg(svg);
// const elements = parseElementsFromSvgUsingRegex(svg);
console.log(elements);
const format = url.searchParams.get("format") ?? "json";
if (format == "json") {
return Response.json({ ok: true, account, elements });
} else {
const contents = elements.map((a) => a.content);
return new Response(contents.join("\n"));
}
}