-
Notifications
You must be signed in to change notification settings - Fork 9
[Waiting] mod: Lit protocol token gated casts #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
49543ce
076b879
fc19161
0cafd6d
4344462
3543d13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"typescript.disableAutomaticTypeAcquisition": true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,8 @@ | ||
module.exports = { | ||
reactStrictMode: true, | ||
transpilePackages: ["@mod-protocol/react"], | ||
transpilePackages: [ | ||
"@mod-protocol/react", | ||
// Fixes https://discord.com/channels/896185694857343026/1174716239508156496 | ||
"@lit-protocol/bls-sdk", | ||
], | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
export const dynamic = "force-dynamic"; | ||
|
||
import { NextResponse, NextRequest } from "next/server"; | ||
import { NFTMetadata, UrlMetadata } from "@mod-protocol/core"; | ||
import { db } from "../lib/db"; | ||
import { chainById } from "../lib/chain-index"; | ||
|
||
export async function POST(request: NextRequest) { | ||
try { | ||
const urls = await request.json(); | ||
|
||
// todo: consider normalizing urls here? currently clients are responsible for doing this. | ||
|
||
// Fetch metadata for each url | ||
const metadata = await db | ||
.selectFrom("url_metadata") | ||
.where("url_metadata.url", "in", urls) | ||
.leftJoin( | ||
"nft_metadata", | ||
"nft_metadata.id", | ||
"url_metadata.nft_metadata_id" | ||
) | ||
.leftJoin( | ||
"nft_collections", | ||
"url_metadata.nft_collection_id", | ||
"nft_collections.id" | ||
) | ||
.select([ | ||
/* Select all columns with aliases to prevent collisions */ | ||
|
||
// URL metadata | ||
"url_metadata.image_url as url_image_url", | ||
"url_metadata.image_height as url_image_height", | ||
"url_metadata.image_width as url_image_width", | ||
"url_metadata.alt as url_alt", | ||
"url_metadata.url as url_url", | ||
"url_metadata.description as url_description", | ||
"url_metadata.title as url_title", | ||
"url_metadata.publisher as url_publisher", | ||
"url_metadata.logo_url as url_logo_url", | ||
"url_metadata.mime_type as url_mime_type", | ||
"url_metadata.nft_collection_id as nft_collection_id", | ||
"url_metadata.nft_metadata_id as nft_metadata_id", | ||
|
||
// NFT Collection metadata | ||
"nft_collections.creator_address as collection_creator_address", | ||
"nft_collections.description as collection_description", | ||
"nft_collections.image_url as collection_image_url", | ||
"nft_collections.item_count as collection_item_count", | ||
"nft_collections.mint_url as collection_mint_url", | ||
"nft_collections.name as collection_name", | ||
"nft_collections.open_sea_url as collection_open_sea_url", | ||
"nft_collections.owner_count as collection_owner_count", | ||
|
||
// NFT metadata | ||
"nft_metadata.token_id as nft_token_id", | ||
"nft_metadata.media_url as nft_media_url", | ||
]) | ||
.execute(); | ||
|
||
const rowsFormatted = metadata.map((row) => { | ||
let nftMetadata: NFTMetadata | undefined; | ||
|
||
if (row.nft_collection_id) { | ||
const [, , prefixAndChainId, prefixAndContractAddress, tokenId] = | ||
row.nft_collection_id.split("/"); | ||
|
||
const [, chainId] = prefixAndChainId.split(":"); | ||
const [, contractAddress] = prefixAndContractAddress.split(":"); | ||
|
||
const chain = chainById[chainId]; | ||
|
||
nftMetadata = { | ||
mediaUrl: row.nft_media_url || undefined, | ||
tokenId: row.nft_token_id || undefined, | ||
collection: { | ||
chain: chain.network, | ||
contractAddress, | ||
creatorAddress: row.collection_creator_address, | ||
description: row.collection_description, | ||
id: row.nft_collection_id, | ||
imageUrl: row.collection_image_url, | ||
itemCount: row.collection_item_count, | ||
mintUrl: row.collection_mint_url, | ||
name: row.collection_name, | ||
openSeaUrl: row.collection_open_sea_url || undefined, | ||
ownerCount: row.collection_owner_count || undefined, | ||
creator: undefined, // TODO: Look up farcaster user by FID | ||
}, | ||
}; | ||
} | ||
|
||
const urlMetadata: UrlMetadata = { | ||
image: row.url_image_url | ||
? { | ||
url: row.url_image_url, | ||
height: row.url_image_height || undefined, | ||
width: row.url_image_width || undefined, | ||
} | ||
: undefined, | ||
alt: row.url_alt || undefined, | ||
description: row.url_description || undefined, | ||
title: row.url_title || undefined, | ||
publisher: row.url_publisher || undefined, | ||
logo: row.url_logo_url ? { url: row.url_logo_url } : undefined, | ||
mimeType: row.url_mime_type || undefined, | ||
nft: nftMetadata, | ||
}; | ||
|
||
return { | ||
url: row.url_url, | ||
urlMetadata, | ||
}; | ||
}); | ||
|
||
const metadataByUrl: { | ||
[key: string]: UrlMetadata; | ||
} = rowsFormatted.reduce((acc, cur) => { | ||
return { | ||
...acc, | ||
[cur.url]: cur.urlMetadata, | ||
}; | ||
}, {}); | ||
|
||
return NextResponse.json(metadataByUrl); | ||
} catch (err) { | ||
console.error(err); | ||
return NextResponse.json({ message: err.message }, { status: err.status }); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { NextRequest, NextResponse } from "next/server"; | ||
import * as LitJsSdk from "@lit-protocol/lit-node-client"; | ||
|
||
async function getLitNodeClient() { | ||
const litNodeClient = new LitJsSdk.LitNodeClient({ | ||
alertWhenUnauthorized: false, | ||
litNetwork: "cayenne", | ||
}); | ||
await litNodeClient.connect(); | ||
|
||
return litNodeClient; | ||
} | ||
|
||
async function decryptData({ | ||
authSig, | ||
ciphertext, | ||
dataToEncryptHash, | ||
accessControlConditions, | ||
}): Promise<null | string> { | ||
const litNodeClient = await getLitNodeClient(); | ||
|
||
let decryptedString; | ||
try { | ||
decryptedString = await LitJsSdk.decryptToString( | ||
{ | ||
authSig, | ||
accessControlConditions, | ||
ciphertext, | ||
dataToEncryptHash, | ||
// FIXME | ||
chain: "ethereum", | ||
}, | ||
litNodeClient | ||
); | ||
} catch (e) { | ||
console.error(e); | ||
return null; | ||
} | ||
|
||
return decryptedString; | ||
} | ||
|
||
export async function POST(request: NextRequest) { | ||
const { | ||
authSig, | ||
payload: { | ||
cipherTextRetrieved, | ||
dataToEncryptHashRetrieved, | ||
accessControlConditions, | ||
}, | ||
} = await request.json(); | ||
|
||
// 4. Decrypt data | ||
const decryptedString = await decryptData({ | ||
ciphertext: cipherTextRetrieved, | ||
authSig: { ...authSig, derivedVia: "web3.eth.personal.sign" }, | ||
dataToEncryptHash: dataToEncryptHashRetrieved, | ||
accessControlConditions, | ||
}); | ||
|
||
if (decryptedString === null) { | ||
return NextResponse.json( | ||
{ | ||
message: "An unknown error occurred", | ||
}, | ||
{ status: 500 } | ||
); | ||
} | ||
|
||
return NextResponse.json({ | ||
decryptedString: "", | ||
}); | ||
} | ||
|
||
// needed for preflight requests to succeed | ||
export const OPTIONS = async (request: NextRequest) => { | ||
return NextResponse.json({}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
import { NextRequest, NextResponse } from "next/server"; | ||
import Irys from "@irys/sdk"; | ||
import * as LitJsSdk from "@lit-protocol/lit-node-client"; | ||
import { AccessControlConditions, AuthSig } from "@lit-protocol/types"; | ||
import { generatePrivateKey } from "viem/accounts"; | ||
import { getAddress } from "viem"; | ||
|
||
/** | ||
* Built using the guide | ||
* https://developer.litprotocol.com/v3/integrations/storage/irys/ | ||
*/ | ||
|
||
async function getLitNodeClient() { | ||
// Initialize LitNodeClient | ||
const litNodeClient = new LitJsSdk.LitNodeClient({ | ||
alertWhenUnauthorized: true, | ||
litNetwork: "cayenne", | ||
}); | ||
await litNodeClient.connect(); | ||
|
||
return litNodeClient; | ||
} | ||
|
||
function getAccessControlConditions({ | ||
chain, | ||
contract, | ||
tokens, | ||
standardContractType, | ||
}: { | ||
chain: string; | ||
contract: string; | ||
tokens: string; | ||
standardContractType: "ERC721" | "ERC1155"; | ||
}): AccessControlConditions { | ||
return [ | ||
{ | ||
conditionType: "evmBasic" as const, | ||
contractAddress: getAddress(contract), | ||
standardContractType: standardContractType, | ||
chain: chain, | ||
method: "balanceOf", | ||
parameters: [":userAddress"], | ||
returnValueTest: { | ||
comparator: ">=", | ||
value: tokens, | ||
}, | ||
}, | ||
]; | ||
} | ||
|
||
async function encryptData({ | ||
dataToEncrypt, | ||
authSig, | ||
chain, | ||
standardContractType, | ||
contract, | ||
tokens, | ||
}: { | ||
dataToEncrypt: string; | ||
authSig: AuthSig; | ||
chain: string; | ||
standardContractType: "ERC721" | "ERC1155"; | ||
contract: string; | ||
tokens: string; | ||
}) { | ||
const accessControlConditions: AccessControlConditions = | ||
getAccessControlConditions({ | ||
chain, | ||
contract, | ||
standardContractType, | ||
tokens, | ||
}); | ||
const litNodeClient = await getLitNodeClient(); | ||
|
||
// 1. Encryption | ||
// <Blob> encryptedString | ||
// <Uint8Array(32)> dataToEncryptHash` | ||
const { ciphertext, dataToEncryptHash } = await LitJsSdk.encryptString( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assuming encryption is happening on server to save client bundle size? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, also Mini-apps can't load arbitrary JS today There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. theoretically this backend function here could be run a secure JS env on a client; not sure how much their lib depends on node apis directly |
||
{ | ||
authSig, | ||
accessControlConditions, | ||
dataToEncrypt: dataToEncrypt, | ||
chain, | ||
}, | ||
litNodeClient | ||
); | ||
return { ciphertext, dataToEncryptHash, accessControlConditions }; | ||
} | ||
|
||
async function storeOnIrys(jsonPayload: object): Promise<string | null> { | ||
const irys = await getIrys(); | ||
|
||
try { | ||
const receipt = await irys.upload(JSON.stringify(jsonPayload), { | ||
tags: [{ name: "Content-Type", value: "application/json" }], | ||
}); | ||
|
||
return receipt.id; | ||
} catch (e) { | ||
console.log("Error uploading data ", e); | ||
return null; | ||
} | ||
} | ||
|
||
async function getIrys() { | ||
// Uint8Array with length 32 | ||
// const key = Uint8Array.from( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment can be removed |
||
// Buffer.from( | ||
// bip39.mnemonicToSeedSync(process.env.PRIVATE_KEY).toString("hex"), | ||
// "hex" | ||
// ) | ||
// ); | ||
// if (!bip39.validateMnemonic(process.env.PRIVATE_KEY)) { | ||
// throw new Error("Invalid mnemonic"); | ||
// } | ||
// mnemonicToAccount(process.env.PRIVATE_KEY); | ||
// const key = (await bip39.mnemonicToSeed(process.env.PRIVATE_KEY)).toString( | ||
// "hex" | ||
// ); | ||
const key = generatePrivateKey(); | ||
|
||
const irys = new Irys({ | ||
url: "https://node2.irys.xyz", | ||
// url: "https://node2.irys.xyz", // URL of the node you want to connect to | ||
token: "matic", // Token used for payment | ||
// under 100kb is free. | ||
key: key, | ||
config: { providerUrl: "https://polygon-mainnet.infura.io" }, // Optional provider URL, only required when using Devnet | ||
}); | ||
|
||
// try { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can be removed if debug comment |
||
// const fundTx = await irys.fund(irys.utils.toAtomic(0.05)); | ||
// console.log( | ||
// `Successfully funded ${irys.utils.fromAtomic(fundTx.quantity)} ${ | ||
// irys.token | ||
// }` | ||
// ); | ||
// } catch (e) { | ||
// console.log("Error uploading data ", e); | ||
// } | ||
|
||
return irys; | ||
} | ||
|
||
// encrypts the text | ||
export async function POST(request: NextRequest) { | ||
try { | ||
const { | ||
// await signAuthMessage(); | ||
authSig, | ||
messageToEncrypt, | ||
standardContractType = "ERC721", | ||
chain = "ethereum", | ||
// https://developer.litprotocol.com/v3/sdk/access-control/evm/basic-examples#must-be-a-member-of-a-dao-molochdaov21-also-supports-daohaus | ||
// https://lit-share-modal-v3-playground.netlify.app/ | ||
tokens = "1", | ||
contract, | ||
} = await request.json(); | ||
|
||
// TODO: Zod validation to get types | ||
|
||
const { ciphertext, dataToEncryptHash, accessControlConditions } = | ||
await encryptData({ | ||
chain, | ||
tokens, | ||
contract, | ||
standardContractType, | ||
authSig: { ...authSig, derivedVia: "web3.eth.personal.sign" }, | ||
dataToEncrypt: messageToEncrypt, | ||
}); | ||
|
||
const irysTransactionId = await storeOnIrys( | ||
createSchemaMetadata({ | ||
cipherText: ciphertext, | ||
dataToEncryptHash, | ||
accessControlConditions: accessControlConditions, | ||
}) | ||
); | ||
|
||
if (!irysTransactionId) { | ||
return new Response(null, { | ||
status: 500, | ||
}); | ||
} | ||
|
||
console.log(irysTransactionId); | ||
|
||
return NextResponse.json({ | ||
url: `${process.env.GATEWAY_URL}/f/embed/${encodeURIComponent( | ||
// https://github.com/ChainAgnostic/namespaces/blob/main/arweave/caip2.md | ||
`arweave:7wIU:${irysTransactionId}` | ||
// `https://gateway.irys.xyz/${irysTransactionId}` | ||
)}`, | ||
}); | ||
} catch (err) { | ||
console.error(err); | ||
return new Response(null, { | ||
status: 500, | ||
}); | ||
} | ||
} | ||
|
||
// needed for preflight requests to succeed | ||
export const OPTIONS = async (request: NextRequest) => { | ||
return NextResponse.json({}); | ||
}; | ||
|
||
function createSchemaMetadata(payload: { | ||
cipherText: string; | ||
dataToEncryptHash: string; | ||
accessControlConditions: AccessControlConditions; | ||
}) { | ||
return { | ||
"@context": ["https://schema.org", "https://schema.modprotocol.org"], | ||
"@type": "WebPage", | ||
name: "Token gated content", | ||
image: "https://i.imgur.com/RO76xMR.png", | ||
description: | ||
"It looks like this app doesn't support Mods yet, but if it did, you'd see the Mod here. Click here to unlock the content if you have access", | ||
"mod:model": { | ||
// unique identifier for the renderer of this miniapp | ||
"@type": "schema.modprotocol.org/lit-protocol/0.0.1/EncryptedData", | ||
payload, | ||
}, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { NextResponse, NextRequest } from "next/server"; | ||
|
||
export async function GET( | ||
request: NextRequest | ||
): Promise<NextResponse<Array<{ value: any; label: string }>>> { | ||
const wallet_address = request.nextUrl.searchParams.get("wallet_address"); | ||
const q = request.nextUrl.searchParams.get("q") || ""; | ||
|
||
// paginate over all of them | ||
let hasNext = true; | ||
const allResults: any[] = []; | ||
let query = `https://api.simplehash.com/api/v0/nfts/owners?chains=polygon,ethereum,optimism&wallet_addresses=${wallet_address}&limit=50`; | ||
while (hasNext) { | ||
const response = await fetch(query, { | ||
method: "GET", | ||
headers: { | ||
accept: "application/json", | ||
"X-API-KEY": process.env.SIMPLEHASH_API_KEY, | ||
}, | ||
}) | ||
.then((response) => response.json()) | ||
.catch((err) => console.error(err)); | ||
|
||
allResults.push(...response.nfts); | ||
if (response.next) { | ||
query = response.next; | ||
} else { | ||
hasNext = false; | ||
} | ||
} | ||
|
||
// todo: remove spam tokens | ||
return NextResponse.json( | ||
allResults | ||
.filter((nft) => | ||
nft?.contract?.name?.toLowerCase().includes(q?.toLowerCase()) | ||
) | ||
.map((nft) => ({ | ||
label: nft.contract.name, | ||
value: { | ||
contract_type: nft.contract.type, | ||
chain: nft.chain, | ||
contract_address: nft.contract_address, | ||
image: nft.previews.image_small_url, | ||
description: nft.description, | ||
}, | ||
})) | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { UrlMetadata } from "@mod-protocol/core"; | ||
import { UrlHandler } from "../../types/url-handler"; | ||
|
||
async function handleArweave(url: string): Promise<UrlMetadata | null> { | ||
// `https://gateway.irys.xyz/${irysTransactionId}` | ||
const transactionId = url.split(":")[2]; | ||
|
||
const reformattedUrl = `https://gateway.irys.xyz/${transactionId}`; | ||
|
||
const response = await fetch(reformattedUrl, { | ||
method: "HEAD", | ||
}); | ||
|
||
// Get content-type | ||
const mimeType = response.headers.get("content-type"); | ||
|
||
if (mimeType === "application/json") { | ||
try { | ||
const arweaveData = await fetch(reformattedUrl); | ||
|
||
const body = await arweaveData.json(); | ||
|
||
// Check for schema | ||
if (body["@type"] === "WebPage") | ||
return { | ||
image: { | ||
url: body.image, | ||
}, | ||
"json-ld": { WebPage: [body] }, | ||
description: body.description, | ||
alt: body.name, | ||
title: body.name, | ||
logo: { | ||
url: body.image, | ||
}, | ||
mimeType: "application/json", | ||
}; | ||
} catch (err) { | ||
console.error(err); | ||
} | ||
} | ||
// TODO: handle html | ||
|
||
return null; | ||
} | ||
|
||
const urlHandler: UrlHandler = { | ||
name: "Arweave", | ||
matchers: ["arweave:7wIU:*"], | ||
handler: handleArweave, | ||
}; | ||
|
||
export default urlHandler; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
NEXT_PUBLIC_API_URL="http://localhost:3001/api" | ||
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= | ||
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="" | ||
NEXT_PUBLIC_URL="http://localhost:3000" | ||
NEXT_PUBLIC_HOST="localhost:3000" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,4 +37,4 @@ | |
"tsconfig": "*", | ||
"typescript": "^5.2.2" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as default } from "./src/manifest"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"name": "@miniapps/lit-protocol-renderer", | ||
"main": "./index.ts", | ||
"types": "./index.ts", | ||
"version": "0.0.1", | ||
"private": true, | ||
"dependencies": { | ||
"@mod-protocol/core": "^0.0.2" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { ModElement } from "@mod-protocol/core"; | ||
|
||
const decrypt: ModElement[] = [ | ||
{ | ||
type: "vertical-layout", | ||
elements: [], | ||
onload: { | ||
ref: "decryption-request", | ||
type: "POST", | ||
body: { | ||
json: { | ||
type: "object", | ||
value: { | ||
authSig: { | ||
type: "object", | ||
value: { | ||
sig: { | ||
type: "string", | ||
value: "{{refs.authSig.signature}}", | ||
}, | ||
signedMessage: { | ||
type: "string", | ||
value: "{{refs.authSig.signedMessage}}", | ||
}, | ||
address: { | ||
type: "string", | ||
value: "{{refs.authSig.address}}", | ||
}, | ||
}, | ||
}, | ||
payload: { | ||
type: "object", | ||
value: { | ||
cipherTextRetrieved: { | ||
type: "string", | ||
value: | ||
"{{embed.metadata.json-ld.WebPage[0].mod:model.payload.cipherText}}", | ||
}, | ||
dataToEncryptHashRetrieved: { | ||
type: "string", | ||
value: | ||
"{{embed.metadata.json-ld.WebPage[0].mod:model.payload.dataToEncryptHash}}", | ||
}, | ||
accessControlConditions: { | ||
type: "array", | ||
value: [ | ||
{ | ||
type: "object", | ||
value: { | ||
conditionType: { | ||
type: "string", | ||
value: | ||
"{{embed.metadata.json-ld.WebPage[0].mod:model.payload.accessControlConditions[0].conditionType}}", | ||
}, | ||
contractAddress: { | ||
type: "string", | ||
value: | ||
"{{embed.metadata.json-ld.WebPage[0].mod:model.payload.accessControlConditions[0].contractAddress}}", | ||
}, | ||
standardContractType: { | ||
type: "string", | ||
value: | ||
"{{embed.metadata.json-ld.WebPage[0].mod:model.payload.accessControlConditions[0].standardContractType}}", | ||
}, | ||
chain: { | ||
type: "string", | ||
value: | ||
"{{embed.metadata.json-ld.WebPage[0].mod:model.payload.accessControlConditions[0].chain}}", | ||
}, | ||
method: { | ||
type: "string", | ||
value: | ||
"{{embed.metadata.json-ld.WebPage[0].mod:model.payload.accessControlConditions[0].method}}", | ||
}, | ||
parameters: { | ||
type: "array", | ||
value: [ | ||
{ | ||
type: "string", | ||
value: | ||
"{{embed.metadata.json-ld.WebPage[0].mod:model.payload.accessControlConditions[0].parameters[0]}}", | ||
}, | ||
], | ||
}, | ||
returnValueTest: { | ||
type: "object", | ||
value: { | ||
comparator: { | ||
type: "string", | ||
value: | ||
"{{embed.metadata.json-ld.WebPage[0].mod:model.payload.accessControlConditions[0].returnValueTest.comparator}}", | ||
}, | ||
value: { | ||
type: "string", | ||
value: | ||
"{{embed.metadata.json-ld.WebPage[0].mod:model.payload.accessControlConditions[0].returnValueTest.value}}", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
url: "{{api}}/lit-protocol-renderer", | ||
onsuccess: "#success", | ||
onerror: "#error", | ||
onloading: "#loading", | ||
}, | ||
}, | ||
]; | ||
export default decrypt; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { ModElement } from "@mod-protocol/core"; | ||
|
||
const error: ModElement[] = [ | ||
{ | ||
type: "text", | ||
label: "Failed to decrypt", | ||
}, | ||
// Buy token link? | ||
{ | ||
type: "button", | ||
label: "Get a token", | ||
onclick: { | ||
type: "OPENLINK", | ||
url: "https://mint.fun/{{embed.metadata.json-ld.WebPage[0].mod:model.payload.accessControlConditions[0].chain}}/{{embed.metadata.json-ld.WebPage[0].mod:model.payload.accessControlConditions[0].contractAddress}}", | ||
onsuccess: "#error", | ||
onerror: "#error", | ||
onloading: "#error", | ||
}, | ||
}, | ||
]; | ||
|
||
export default error; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { ModElement } from "@mod-protocol/core"; | ||
|
||
const loading: ModElement[] = [ | ||
{ | ||
type: "text", | ||
label: "Decrypting", | ||
}, | ||
{ | ||
type: "circular-progress", | ||
}, | ||
]; | ||
|
||
export default loading; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { ModManifest } from "@mod-protocol/core"; | ||
import loading from "./loading"; | ||
import rendering from "./rendering"; | ||
import error from "./error"; | ||
import success from "./success"; | ||
import decrypt from "./decrypt"; | ||
|
||
const manifest: ModManifest = { | ||
slug: "lit-protocol-renderer", | ||
name: "Read token gated casts", | ||
custodyAddress: "furlong.eth", | ||
logo: "https://openseauserdata.com/files/2105703ca9fbe5116c26b9967a596abe.png", | ||
custodyGithubUsername: "davidfurlong", | ||
version: "0.0.1", | ||
contentEntrypoints: rendering, | ||
elements: { | ||
"#loading": loading, | ||
"#decrypt": decrypt, | ||
"#error": error, | ||
"#success": success, | ||
}, | ||
}; | ||
|
||
export default manifest; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { ModConditionalElement } from "@mod-protocol/core"; | ||
|
||
const rendering: ModConditionalElement[] = [ | ||
{ | ||
if: { | ||
value: "{{embed.metadata.json-ld.WebPage[0].mod:model.@type}}", | ||
match: { | ||
equals: "schema.modprotocol.org/lit-protocol/0.0.1/EncryptedData", | ||
}, | ||
}, | ||
element: [ | ||
{ | ||
type: "card", | ||
aspectRatio: 1200 / 630, | ||
imageSrc: "{{embed.metadata.image.url}}", | ||
elements: [ | ||
{ | ||
type: "button", | ||
loadingLabel: "Sign the message", | ||
label: "Sign to decrypt the secret message", | ||
onclick: { | ||
onsuccess: "#decrypt", | ||
onerror: "#error", | ||
type: "web3.eth.personal.sign", | ||
ref: "authSig", | ||
data: { | ||
// domain: "localhost:3000", | ||
// address: "{{user.wallet.address}}", | ||
statement: | ||
"You are signing a message to prove you own this account", | ||
// uri: "http://localhost:3000", | ||
version: "1", | ||
// FIXME | ||
chainId: "1", | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
]; | ||
export default rendering; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { ModElement } from "@mod-protocol/core"; | ||
|
||
const success: ModElement[] = [ | ||
{ | ||
type: "horizontal-layout", | ||
elements: [ | ||
{ | ||
type: "avatar", | ||
src: "https://cdn-icons-png.flaticon.com/512/102/102288.png", | ||
}, | ||
{ | ||
type: "text", | ||
label: "{{refs.decryption-request.response.data.decryptedString}}", | ||
}, | ||
], | ||
}, | ||
]; | ||
|
||
export default success; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"extends": "tsconfig/base.json", | ||
"include": ["."], | ||
"exclude": ["dist", "build", "node_modules"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as default } from "./src/manifest"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"name": "@miniapps/lit-protocol", | ||
"main": "./index.ts", | ||
"types": "./index.ts", | ||
"version": "0.0.1", | ||
"private": true, | ||
"dependencies": { | ||
"@mod-protocol/core": "^0.0.2" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { ModElement } from "@mod-protocol/core"; | ||
|
||
const action: ModElement[] = [ | ||
{ | ||
type: "vertical-layout", | ||
elements: [ | ||
{ | ||
type: "textarea", | ||
ref: "plaintext", | ||
placeholder: "Content to token gate", | ||
}, | ||
Comment on lines
+7
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason why this doesn't use the cast text content? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good question - the cast contents includes mentions (that won't work) and special renderers (like textcuts) that Mini-apps can't directly use. Additionally casts include embeds that we can't easily render if we added them to the encrypted contents, and token gated casts would probably still include or be able to include the cast body; so it'd be a bit confusing to have the cast text appearing/disappearing, or for it to somehow be encrypted when submitted. In short, I'm not sure our abstractions are particularly close to supporting it in a graceful manner |
||
{ | ||
type: "combobox", | ||
onload: { | ||
ref: "getOptions", | ||
type: "GET", | ||
onsuccess: "#action", | ||
onerror: "#error", | ||
url: "{{api}}/lit-protocol/search-nfts?wallet_address={{user.wallet.address}}&q={{refs.contract.value}}", | ||
}, | ||
ref: "contract", | ||
valueRef: "selectedContract", | ||
optionsRef: "refs.getOptions.response.data", | ||
placeholder: "Pick token to gate by", | ||
}, | ||
{ | ||
type: "button", | ||
label: "Publish", | ||
onclick: { | ||
type: "POST", | ||
ref: "encryption", | ||
url: "{{api}}/lit-protocol", | ||
body: { | ||
json: { | ||
type: "object", | ||
value: { | ||
authSig: { | ||
type: "object", | ||
value: { | ||
sig: { | ||
type: "string", | ||
value: "{{refs.authSig.signature}}", | ||
}, | ||
signedMessage: { | ||
type: "string", | ||
value: "{{refs.authSig.signedMessage}}", | ||
}, | ||
address: { | ||
type: "string", | ||
value: "{{refs.authSig.address}}", | ||
}, | ||
}, | ||
}, | ||
standardContractType: { | ||
type: "string", | ||
value: "{{refs.selectedContract.value.contract_type}}", | ||
}, | ||
messageToEncrypt: { | ||
type: "string", | ||
value: "{{refs.plaintext.value}}", | ||
}, | ||
chain: { | ||
type: "string", | ||
value: "{{refs.selectedContract.value.chain}}", | ||
}, | ||
tokens: { | ||
type: "string", | ||
value: "1", | ||
}, | ||
contract: { | ||
type: "string", | ||
value: "{{refs.selectedContract.value.contract_address}}", | ||
}, | ||
}, | ||
}, | ||
}, | ||
onsuccess: { | ||
type: "ADDEMBED", | ||
url: "{{refs.encryption.response.data.url}}", | ||
name: "Encrypted data", | ||
mimeType: "application/ld+json", | ||
onsuccess: { | ||
type: "EXIT", | ||
}, | ||
}, | ||
onloading: "#loading", | ||
onerror: "#error", | ||
}, | ||
}, | ||
{ | ||
type: "button", | ||
variant: "secondary", | ||
label: "Manual entry", | ||
onclick: "#advanced-form", | ||
}, | ||
], | ||
}, | ||
]; | ||
|
||
export default action; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { ModElement } from "@mod-protocol/core"; | ||
|
||
const action: ModElement[] = [ | ||
{ | ||
type: "vertical-layout", | ||
elements: [ | ||
{ | ||
type: "input", | ||
ref: "plaintext", | ||
placeholder: "Content to token gate", | ||
}, | ||
{ | ||
type: "select", | ||
options: [ | ||
{ | ||
value: "ethereum", | ||
label: "Ethereum", | ||
}, | ||
{ | ||
value: "optimism", | ||
label: "Optimism", | ||
}, | ||
{ | ||
value: "polygon", | ||
label: "Polygon", | ||
}, | ||
], | ||
ref: "chain", | ||
placeholder: "Chain", | ||
}, | ||
{ | ||
type: "select", | ||
options: [ | ||
{ | ||
value: "ERC721", | ||
label: "ERC721", | ||
}, | ||
{ | ||
value: "ERC1155", | ||
label: "ERC1155", | ||
}, | ||
], | ||
ref: "standardContractType", | ||
placeholder: "Contract type", | ||
}, | ||
{ | ||
type: "input", | ||
isClearable: true, | ||
ref: "contract", | ||
placeholder: "Contract address", | ||
}, | ||
{ | ||
type: "button", | ||
label: "Publish", | ||
onclick: { | ||
type: "POST", | ||
ref: "encryption", | ||
url: "{{api}}/lit-protocol", | ||
body: { | ||
json: { | ||
type: "object", | ||
value: { | ||
authSig: { | ||
type: "object", | ||
value: { | ||
sig: { | ||
type: "string", | ||
value: "{{refs.authSig.signature}}", | ||
}, | ||
signedMessage: { | ||
type: "string", | ||
value: "{{refs.authSig.signedMessage}}", | ||
}, | ||
address: { | ||
type: "string", | ||
value: "{{refs.authSig.address}}", | ||
}, | ||
}, | ||
}, | ||
standardContractType: { | ||
type: "string", | ||
value: "{{refs.standardContractType.value}}", | ||
}, | ||
messageToEncrypt: { | ||
type: "string", | ||
value: "{{refs.plaintext.value}}", | ||
}, | ||
chain: { | ||
type: "string", | ||
value: "{{refs.chain.value}}", | ||
}, | ||
tokens: { | ||
type: "string", | ||
value: "1", | ||
}, | ||
contract: { | ||
type: "string", | ||
value: "{{refs.contract.value}}", | ||
}, | ||
}, | ||
}, | ||
}, | ||
onsuccess: { | ||
type: "ADDEMBED", | ||
url: "{{refs.encryption.response.data.url}}", | ||
name: "Encrypted data", | ||
mimeType: "application/ld+json", | ||
onsuccess: { | ||
type: "EXIT", | ||
}, | ||
}, | ||
onloading: "#loading", | ||
onerror: { | ||
// fixme: show error | ||
type: "EXIT", | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
]; | ||
|
||
export default action; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { ModElement } from "@mod-protocol/core"; | ||
|
||
const error: ModElement[] = [ | ||
{ | ||
type: "text", | ||
label: "ERROR: Something went wrong", | ||
}, | ||
{ | ||
type: "button", | ||
label: "Retry", | ||
onclick: "#sign", | ||
}, | ||
]; | ||
|
||
export default error; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { ModElement } from "@mod-protocol/core"; | ||
|
||
const loading: ModElement[] = [ | ||
{ | ||
type: "text", | ||
label: "Encrypting", | ||
}, | ||
{ | ||
type: "circular-progress", | ||
}, | ||
]; | ||
|
||
export default loading; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { ModManifest } from "@mod-protocol/core"; | ||
import action from "./action"; | ||
import loading from "./loading"; | ||
import error from "./error"; | ||
import sign from "./sign"; | ||
import advancedForm from "./advanced-form"; | ||
|
||
const manifest: ModManifest = { | ||
slug: "lit-protocol", | ||
name: "Token gate", | ||
custodyAddress: "furlong.eth", | ||
logo: "https://openseauserdata.com/files/2105703ca9fbe5116c26b9967a596abe.png", | ||
custodyGithubUsername: "davidfurlong", | ||
version: "0.0.1", | ||
creationEntrypoints: sign, | ||
permissions: ["web3.eth.personal.sign"], | ||
modelDefinitions: { | ||
EncryptedData: { | ||
type: "object", | ||
properties: { | ||
cipherText: { type: "string" }, | ||
dataToEncryptHash: { type: "string" }, | ||
accessControlConditions: { | ||
type: "array", | ||
minItems: 1, | ||
items: { | ||
type: "object", | ||
properties: { | ||
conditionType: { type: "string" }, | ||
contractAddress: { type: "string" }, | ||
standardContractType: { type: "string" }, | ||
chain: { type: "string" }, | ||
method: { type: "string" }, | ||
parameters: { | ||
type: "array", | ||
minItems: 1, | ||
items: { type: "string" }, | ||
}, | ||
returnValueTest: { | ||
type: "object", | ||
properties: { | ||
comparator: { type: "string" }, | ||
value: { type: "string" }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
elements: { | ||
"#sign": sign, | ||
"#error": error, | ||
"#action": action, | ||
"#loading": loading, | ||
"#advanced-form": advancedForm, | ||
}, | ||
}; | ||
|
||
export default manifest; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { ModElement } from "@mod-protocol/core"; | ||
|
||
const action: ModElement[] = [ | ||
{ | ||
type: "vertical-layout", | ||
elements: [ | ||
{ | ||
type: "text", | ||
label: "Sign to prove ownership of this account", | ||
}, | ||
{ | ||
type: "button", | ||
label: "Sign", | ||
onclick: { | ||
onsuccess: "#action", | ||
onerror: "#error", | ||
type: "web3.eth.personal.sign", | ||
ref: "authSig", | ||
data: { | ||
statement: "", | ||
version: "1", | ||
// FIXME | ||
chainId: "1", | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
]; | ||
|
||
export default action; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"extends": "tsconfig/base.json", | ||
"include": ["."], | ||
"exclude": ["dist", "build", "node_modules"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,8 @@ export function isVideoEmbed(embed: Embed) { | |
export function hasFullSizedImage(embed: Embed) { | ||
return ( | ||
embed.metadata?.image?.url && | ||
embed.metadata?.image.width !== embed.metadata.image.height | ||
(embed.metadata?.image.width !== embed.metadata.image.height || | ||
(!embed.metadata?.image.width && !embed.metadata.image.height)) | ||
); | ||
} | ||
|
||
|
@@ -55,6 +56,8 @@ export type UrlMetadata = { | |
width?: number; | ||
height?: number; | ||
}; | ||
// map of schema.org types to arrays of schema.org definitions | ||
"json-ld"?: Record<string, object[]>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason why this property isn't camel cased? Makes referencing it awkward |
||
description?: string; | ||
alt?: string; | ||
title?: string; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
type ModConditionalElement = { | ||
import type { JSONSchema7 } from "json-schema"; | ||
|
||
export type ModConditionalElement = { | ||
element: ModElement[]; | ||
if: ValueOp; | ||
}; | ||
|
@@ -15,10 +17,12 @@ export type ModManifest = { | |
/** A valid url pointing to an image file, it should be a square */ | ||
logo: string; | ||
version: string; | ||
modelDefinitions?: Record<string, JSONSchema7>; | ||
creationEntrypoints?: ModElement[]; | ||
contentEntrypoints?: ModConditionalElement[]; | ||
elements?: Record<string, ModElement[]>; | ||
permissions?: string[]; | ||
// perhaps data.user.wallet.address is better. | ||
permissions?: Array<"user.wallet.address" | "web3.eth.personal.sign">; | ||
Comment on lines
+24
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree |
||
}; | ||
|
||
export type ModEvent = | ||
|
@@ -77,9 +81,10 @@ type HTTPBody = | |
formData: Record<string, FormDataType>; | ||
}; | ||
|
||
type HTTPAction = BaseAction & { url: string } & ( | ||
export type HTTPAction = BaseAction & { url: string } & ( | ||
| { | ||
type: "GET"; | ||
searchParams?: Record<string, string>; | ||
} | ||
| { | ||
type: "POST"; | ||
|
@@ -117,13 +122,24 @@ type OpenLinkAction = BaseAction & { | |
url: string; | ||
}; | ||
|
||
export type EthPersonalSignData = { | ||
statement: string; | ||
version: string; | ||
chainId: string; | ||
}; | ||
|
||
export type EthTransactionData = { | ||
to: string; | ||
from: string; | ||
data?: string; | ||
value?: string; | ||
}; | ||
|
||
type EthPersonalSignAction = BaseAction & { | ||
type: "web3.eth.personal.sign"; | ||
data: EthPersonalSignData; | ||
}; | ||
|
||
type SendEthTransactionAction = BaseAction & { | ||
type: "SENDETHTRANSACTION"; | ||
chainId: string; | ||
|
@@ -147,6 +163,7 @@ export type ModAction = | |
| AddEmbedAction | ||
| SetInputAction | ||
| OpenLinkAction | ||
| EthPersonalSignAction | ||
| SendEthTransactionAction | ||
| ExitAction; | ||
|
||
|
@@ -171,6 +188,7 @@ export type ModElement = | |
| { | ||
type: "button"; | ||
label: string; | ||
loadingLabel?: string; | ||
variant?: "primary" | "secondary" | "destructive"; | ||
onclick: ModEvent; | ||
} | ||
|
@@ -188,10 +206,25 @@ export type ModElement = | |
onload?: ModEvent; | ||
} | ||
| { | ||
type: "textarea"; | ||
ref?: string; | ||
placeholder?: string; | ||
onchange?: ModEvent; | ||
onsubmit?: ModEvent; | ||
} | ||
| { | ||
type: "select"; | ||
options: Array<{ label: string; value: any }>; | ||
ref?: string; | ||
placeholder?: string; | ||
isClearable?: boolean; | ||
onchange?: ModEvent; | ||
} | ||
| { | ||
type: "input"; | ||
ref?: string; | ||
placeholder?: string; | ||
clearable?: boolean; | ||
isClearable?: boolean; | ||
onchange?: ModEvent; | ||
onsubmit?: ModEvent; | ||
} | ||
|
@@ -200,16 +233,27 @@ export type ModElement = | |
videoSrc: string; | ||
} | ||
| { | ||
ref?: string; | ||
type: "tabs"; | ||
ref?: string; | ||
values: string[]; | ||
names: string[]; | ||
onload?: ModEvent; | ||
onchange?: ModEvent; | ||
} | ||
| ({ | ||
| { | ||
type: "combobox"; | ||
ref?: string; | ||
isClearable?: boolean; | ||
placeholder?: string; | ||
optionsRef?: string; | ||
valueRef?: string; | ||
onload?: ModEvent; | ||
onpick?: ModEvent; | ||
onchange?: ModEvent; | ||
} | ||
| ({ | ||
type: "image-grid-list"; | ||
ref?: string; | ||
onload?: ModEvent; | ||
onpick?: ModEvent; | ||
} & ( | ||
|
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
"use client"; | ||
|
||
import * as React from "react"; | ||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; | ||
import * as SelectPrimitive from "@radix-ui/react-select"; | ||
|
||
import { cn } from "lib/utils"; | ||
|
||
const Select = SelectPrimitive.Root; | ||
|
||
const SelectGroup = SelectPrimitive.Group; | ||
|
||
const SelectValue = SelectPrimitive.Value; | ||
|
||
const SelectTrigger = React.forwardRef< | ||
React.ElementRef<typeof SelectPrimitive.Trigger>, | ||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> | ||
>(({ className, children, ...props }, ref) => ( | ||
<SelectPrimitive.Trigger | ||
ref={ref} | ||
className={cn( | ||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50", | ||
className | ||
)} | ||
{...props} | ||
> | ||
{children} | ||
<SelectPrimitive.Icon asChild> | ||
<CaretSortIcon className="h-4 w-4 opacity-50" /> | ||
</SelectPrimitive.Icon> | ||
</SelectPrimitive.Trigger> | ||
)); | ||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; | ||
|
||
const SelectContent = React.forwardRef< | ||
React.ElementRef<typeof SelectPrimitive.Content>, | ||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> | ||
>(({ className, children, position = "popper", ...props }, ref) => ( | ||
<SelectPrimitive.Portal> | ||
<SelectPrimitive.Content | ||
ref={ref} | ||
className={cn( | ||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||
position === "popper" && | ||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", | ||
className | ||
)} | ||
position={position} | ||
{...props} | ||
> | ||
<SelectPrimitive.Viewport | ||
className={cn( | ||
"p-1", | ||
position === "popper" && | ||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" | ||
)} | ||
> | ||
{children} | ||
</SelectPrimitive.Viewport> | ||
</SelectPrimitive.Content> | ||
</SelectPrimitive.Portal> | ||
)); | ||
SelectContent.displayName = SelectPrimitive.Content.displayName; | ||
|
||
const SelectLabel = React.forwardRef< | ||
React.ElementRef<typeof SelectPrimitive.Label>, | ||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> | ||
>(({ className, ...props }, ref) => ( | ||
<SelectPrimitive.Label | ||
ref={ref} | ||
className={cn("px-2 py-1.5 text-sm font-semibold", className)} | ||
{...props} | ||
/> | ||
)); | ||
SelectLabel.displayName = SelectPrimitive.Label.displayName; | ||
|
||
const SelectItem = React.forwardRef< | ||
React.ElementRef<typeof SelectPrimitive.Item>, | ||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> | ||
>(({ className, children, ...props }, ref) => ( | ||
<SelectPrimitive.Item | ||
ref={ref} | ||
className={cn( | ||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||
className | ||
)} | ||
{...props} | ||
> | ||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"> | ||
<SelectPrimitive.ItemIndicator> | ||
<CheckIcon className="h-4 w-4" /> | ||
</SelectPrimitive.ItemIndicator> | ||
</span> | ||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> | ||
</SelectPrimitive.Item> | ||
)); | ||
SelectItem.displayName = SelectPrimitive.Item.displayName; | ||
|
||
const SelectSeparator = React.forwardRef< | ||
React.ElementRef<typeof SelectPrimitive.Separator>, | ||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> | ||
>(({ className, ...props }, ref) => ( | ||
<SelectPrimitive.Separator | ||
ref={ref} | ||
className={cn("-mx-1 my-1 h-px bg-muted", className)} | ||
{...props} | ||
/> | ||
)); | ||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName; | ||
|
||
export { | ||
Select, | ||
SelectGroup, | ||
SelectValue, | ||
SelectTrigger, | ||
SelectContent, | ||
SelectLabel, | ||
SelectItem, | ||
SelectSeparator, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import * as React from "react"; | ||
|
||
import { cn } from "lib/utils"; | ||
|
||
export interface TextareaProps | ||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} | ||
|
||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( | ||
({ className, ...props }, ref) => { | ||
return ( | ||
<textarea | ||
className={cn( | ||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", | ||
className | ||
)} | ||
ref={ref} | ||
{...props} | ||
/> | ||
); | ||
} | ||
); | ||
Textarea.displayName = "Textarea"; | ||
|
||
export { Textarea }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
"use client"; | ||
|
||
import * as React from "react"; | ||
import { Button } from "components/ui/button"; | ||
import { | ||
Command, | ||
CommandEmpty, | ||
CommandGroup, | ||
CommandInput, | ||
CommandItem, | ||
} from "components/ui/command"; | ||
import { Popover, PopoverContent, PopoverTrigger } from "components/ui/popover"; | ||
import { ChevronDownIcon } from "@radix-ui/react-icons"; | ||
|
||
type ResultType<T> = { value: T; label: string }; | ||
|
||
type Props<T> = { | ||
options: Array<ResultType<T>> | null; | ||
onPick: (value: T) => void; | ||
onChange: (value: string) => void; | ||
placeholder?: string; | ||
}; | ||
|
||
export function ComboboxRenderer<T extends number | string = any>( | ||
props: Props<T> | ||
) { | ||
const { options, onChange, onPick } = props; | ||
const [value, setValue] = React.useState<ResultType<T> | null>(null); | ||
|
||
const [open, setOpen] = React.useState(false); | ||
|
||
React.useEffect(() => { | ||
onChange(""); | ||
}, []); | ||
|
||
const handlePick = React.useCallback( | ||
(newValue: ResultType<T>) => { | ||
setOpen(false); | ||
setValue(newValue); | ||
onPick(newValue.value); | ||
}, | ||
[onChange, setOpen, setValue] | ||
); | ||
return ( | ||
<Popover open={open} onOpenChange={setOpen}> | ||
<PopoverTrigger asChild> | ||
<Button | ||
variant="outline" | ||
role="combobox" | ||
aria-expanded={open} | ||
className="w-full justify-between" | ||
> | ||
{value ? value.label : props.placeholder} | ||
<ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent align="start" className="w-[300px] p-0"> | ||
<Command> | ||
<CommandInput placeholder="Search" /> | ||
<CommandEmpty>No results found.</CommandEmpty> | ||
<CommandGroup className="max-h-[300px] overflow-y-auto"> | ||
{!options ? "...loading" : null} | ||
{options?.map((option) => ( | ||
<CommandItem | ||
key={option.label} | ||
value={String(option.label)} | ||
className="cursor-pointer" | ||
onSelect={() => handlePick(option)} | ||
> | ||
{option.label} | ||
</CommandItem> | ||
))} | ||
</CommandGroup> | ||
</Command> | ||
</PopoverContent> | ||
</Popover> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import React from "react"; | ||
import { Renderers } from "@mod-protocol/react"; | ||
import { | ||
Select, | ||
SelectContent, | ||
SelectItem, | ||
SelectTrigger, | ||
SelectValue, | ||
} from "components/ui/select"; | ||
import { Button } from "components/ui/button"; | ||
import { Cross1Icon } from "@radix-ui/react-icons"; | ||
|
||
export const SelectRenderer = ( | ||
props: React.ComponentProps<Renderers["Select"]> | ||
) => { | ||
const { isClearable, placeholder, onChange, options } = props; | ||
const [value, setValue] = React.useState<string>(""); | ||
const selectRef = React.useRef<HTMLSelectElement | null>(null); | ||
return ( | ||
<div className="w-full flex flex-row items-center rounded-md flex-grow"> | ||
<Select | ||
onValueChange={(value: string) => { | ||
onChange(value); | ||
setValue(value); | ||
}} | ||
value={value} | ||
> | ||
<SelectTrigger className="w-[180px]"> | ||
<SelectValue placeholder={placeholder} ref={selectRef} /> | ||
</SelectTrigger> | ||
<SelectContent> | ||
{options.map((option, i) => { | ||
return ( | ||
<SelectItem | ||
value={option.value} | ||
// items are stable | ||
key={i} | ||
> | ||
{option.label} | ||
</SelectItem> | ||
); | ||
})} | ||
</SelectContent> | ||
{isClearable && value ? ( | ||
<Button | ||
type="button" | ||
className="rounded-l-none text-gray-400 hover:bg-transparent" | ||
variant="ghost" | ||
size="icon" | ||
onClick={() => { | ||
onChange(""); | ||
setValue(""); | ||
selectRef.current?.focus(); | ||
}} | ||
> | ||
<Cross1Icon /> | ||
</Button> | ||
) : null} | ||
</Select> | ||
</div> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import React from "react"; | ||
import { Renderers } from "@mod-protocol/react"; | ||
import { Textarea } from "components/ui/textarea"; | ||
|
||
export const TextareaRenderer = ( | ||
props: React.ComponentProps<Renderers["Textarea"]> | ||
) => { | ||
const { placeholder, onChange, onSubmit } = props; | ||
const [value, setValue] = React.useState<string>(""); | ||
const inputRef = React.useRef<HTMLTextAreaElement | null>(null); | ||
return ( | ||
<div className="w-full flex flex-row items-center border rounded-md border-input"> | ||
<Textarea | ||
ref={inputRef} | ||
placeholder={placeholder} | ||
className={"flex-1 border-none"} | ||
onChange={(ev) => { | ||
onChange(ev.currentTarget.value); | ||
setValue(ev.currentTarget.value); | ||
}} | ||
onSubmit={(ev) => { | ||
onSubmit(ev.currentTarget.value); | ||
setValue(ev.currentTarget.value); | ||
}} | ||
value={value} | ||
/> | ||
</div> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { | ||
EthPersonalSignActionResolverInit, | ||
EthPersonalSignActionResolverEvents, | ||
} from "@mod-protocol/core"; | ||
|
||
export default function actionResolverEthPersonalSign( | ||
init: EthPersonalSignActionResolverInit, | ||
events: EthPersonalSignActionResolverEvents | ||
) { | ||
// eslint-disable-next-line no-console | ||
console.warn( | ||
"Please implement 'EthPersonalSignActionResolver' and configure the MiniApp to use it" | ||
); | ||
} |
Large diffs are not rendered by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should consider renaming these endpoints. I suggest
/embed-metadata/by-casts
and/embed-metadata/by-urls
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed, but that means no backwards compat; will have to let integrators know
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed, but that means no backwards compat; will have to let integrators know