From ee69c8e91dc2c9d36a6a4e4743dd858ab9908d77 Mon Sep 17 00:00:00 2001 From: Jack Frain Date: Wed, 6 Nov 2024 16:16:04 -0500 Subject: [PATCH 1/3] feat(specs): if not vouched, vouch before stamping --- src/dal/index.ts | 2 +- src/lib/index.ts | 23 +++++++++++---- src/pages/home/index.tsx | 38 ++++++++++++++++++++++-- src/pages/show/index.tsx | 36 +++++++++++++++++++++- src/services/ao.ts | 64 ++++++++++++++++++++++++++++++++++++++-- src/services/index.ts | 3 +- src/services/vouched.ts | 29 ------------------ src/services/warp.ts | 0 8 files changed, 152 insertions(+), 43 deletions(-) delete mode 100644 src/services/vouched.ts delete mode 100644 src/services/warp.ts diff --git a/src/dal/index.ts b/src/dal/index.ts index 06ab869..b439f0f 100644 --- a/src/dal/index.ts +++ b/src/dal/index.ts @@ -52,7 +52,7 @@ const stampCountSchema = z.function().args(z.string()) const isVouchedSchema = z .function() .args(z.string()) - .returns(z.promise(z.boolean())) + .returns(z.promise(z.object({ addr: z.string(), vouched: z.boolean() }))) const querySchema = z.function().args(z.string()).returns(z.promise(z.array(AoSpecSchema.optional()))) diff --git a/src/lib/index.ts b/src/lib/index.ts index 2f9dafd..67bb76b 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -43,6 +43,7 @@ export default { const query = fromPromise(querySchema.implement(services.query)) const queryRelated = fromPromise(queryRelatedSchema.implement(services.queryRelated)) const stamp = fromPromise(stampSchema.implement(services.stamp)) + const isVouched = fromPromise(isVouchedSchema.implement(services.isVouched)) const stampCounts = fromPromise(stampCountsSchema.implement(services.stampCounts)) return { @@ -161,12 +162,22 @@ export default { ) .map(uniqBy(prop("id"))) }, - stamp: (tx: string) => - connect() - //.chain(isVouched) // isVouched - .chain( - (addr: string) => stamp(tx, addr) - ), + stamp: (tx: string) => { + return connect() + .chain(isVouched) + .chain(({ addr, vouched }) => { + if (vouched) { + return Resolved({ addr }) + } + return Rejected('Not Vouched') + }) + .bichain( + Rejected, + ({ addr }) => { + return stamp(tx, addr) + }, + ) + } } }, } diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index bc2b81f..945b6b1 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -9,6 +9,7 @@ import Loading from '../../components/loading' const HomePage = () => { const [showError, setShowError] = useState(false) + const [showVouchModal, setShowVouchModal] = useState(false) const [copying, setCopying] = useState(false) const s = useHomeService() @@ -16,7 +17,11 @@ const HomePage = () => { const send = s[1] const context = useMemo(() => { if (s[0].context?.error) { - setShowError(true) + if (s[0].context.error.message === 'Not Vouched') { + setShowVouchModal(true) + } else { + setShowError(true) + } } return s[0].context }, [s]) @@ -121,7 +126,7 @@ const HomePage = () => { tx={context?.selected?.id} /> - + setShowError(!showError)} /> {showError && (
@@ -140,6 +145,35 @@ const HomePage = () => {
)} + setShowVouchModal(!showVouchModal)} /> + {showVouchModal && ( +
+
+

You are not vouched!

+
You must be vouched to stamp Specs.
+ + +
+
+ )} ) } diff --git a/src/pages/show/index.tsx b/src/pages/show/index.tsx index c4d22de..4e755f7 100644 --- a/src/pages/show/index.tsx +++ b/src/pages/show/index.tsx @@ -8,11 +8,16 @@ const shortHash = (h: string) => `${take(5, h)}...${takeLast(5, h)}` const ShowPage = ({ tx, parent = false }: { tx: string, parent?: boolean }) => { const [showError, setShowError] = useState(false) + const [showVouchModal, setShowVouchModal] = useState(false) const s = useShowService() const send = s[1] const context = useMemo(() => { if (s[0].context?.error) { - setShowError(true) + if (s[0].context.error.message === 'Not Vouched') { + setShowVouchModal(true) + } else { + setShowError(true) + } } return s[0].context }, [s]) @@ -158,6 +163,35 @@ const ShowPage = ({ tx, parent = false }: { tx: string, parent?: boolean }) => { )} + setShowVouchModal(!showVouchModal)} /> + {showVouchModal && ( +
+
+

You are not vouched!

+
You must be vouched to stamp Specs.
+ + +
+
+ )} ) } diff --git a/src/services/ao.ts b/src/services/ao.ts index 2c00f77..a9855cb 100644 --- a/src/services/ao.ts +++ b/src/services/ao.ts @@ -1,7 +1,15 @@ -import { createDataItemSigner, dryrun, message } from "@permaweb/aoconnect" +import { createDataItemSigner, dryrun, message, result } from "@permaweb/aoconnect" +import { compose, head, propOr } from "ramda" import { Spec } from "src/types/Spec" const SPEC_PID = "6x68KURcD4ySOslFCxiIorjsbpzNy6WD4joH6C8VHgg" - +const VOUCH_PID = "ZTTO02BL2P-lseTLUgiIPD9d0CF1sc4LbMA2AQ7e9jo" +const VOUCHER_WHITELIST = [ + "Ax_uXyLQBPZSQ15movzv9-O1mDo30khslqN64qD27Z8", // Vouch-X + "k6p1MtqYhQQOuTSfN8gH7sQ78zlHavt8dCDL88btn9s", // Vouch-Gitcoin-Passport + "QeXDjjxcui7W2xU08zOlnFwBlbiID4sACpi0tSS3VgY", // Vouch-AO-Balance + "3y0YE11i21hpP8UY0Z1AVhtPoJD4V_AbEBx-g0j9wRc", // Vouch-wAR-Stake +] +const MIN_VOUCH_SCORE = 2 export const upload = async (md: { data: string tags: { @@ -72,6 +80,7 @@ export const upload = async (md: { return result } + export const query = async (tx: string) => { const args = { process: SPEC_PID, @@ -126,4 +135,55 @@ export const queryRelated = async (tx: string) => { const data: Spec[] = JSON.parse(result.Output.data) return data +} + +export const isVouched = async (tx: string) => { + const args = { + process: VOUCH_PID, + tags: [ + { + name: "Action", + value: "Get-Vouches" + }, + { + name: "ID", + value: tx + } + ], + signer: createDataItemSigner(window.arweaveWallet) + } + + const messageId = await message(args) + + const resultArgs = { + process: VOUCH_PID, + message: messageId, + } + const messageResult = await result(resultArgs) + + const vouchers = compose( + propOr({}, 'Vouchers'), + JSON.parse, + propOr('{}', 'Data'), + head, + propOr([], 'Messages') + )(messageResult) + + let vouchScore = 0 + for (const voucher of Object.keys(vouchers)) { + if (VOUCHER_WHITELIST.includes(voucher)) { + const vouch = vouchers[voucher] + const vouchFor = vouch['Vouch-For'] + if (vouchFor != tx) { + throw new Error('Vouch has Vouch-For mismatch') + } + const valueStr = vouch['Value'].match(/^(\d+\.\d+)|(\d+)/g)?.[0] ?? "0" + const value = parseFloat(valueStr) + if (valueStr == null || value == null) { + throw new Error('Vouch has invalid Value') + } + vouchScore += value + } + } + return { addr: tx, vouched: Boolean(vouchScore > MIN_VOUCH_SCORE) } } \ No newline at end of file diff --git a/src/services/index.ts b/src/services/index.ts index 19c8f15..8d54ab9 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,8 +1,7 @@ import { getActiveAddress } from "./wallet"; import { post, gql, get } from "./arweave"; import { stampCounts, stamp, stampCount } from "./stamps"; -import { isVouched } from "./vouched"; -import { query, queryAll, queryRelated, upload } from "./ao" +import { isVouched, query, queryAll, queryRelated, upload } from "./ao" import { Services } from "../dal" diff --git a/src/services/vouched.ts b/src/services/vouched.ts deleted file mode 100644 index 243caf3..0000000 --- a/src/services/vouched.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { getHost } from "./get-host"; -import { path } from "ramda"; - -export const isVouched = (addr) => - fetch(`https://${getHost()}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: ` -query ($addresses: [String!]!) { - transactions(tags:[{name: "Vouch-For", values: $addresses}]) { - edges { - node { - id - } - } - } -} - `, - variables: { - addresses: [addr], - }, - }), - }) - .then((res) => res.json()) - .then(path(["data", "transactions", "edges"])) - .then((edges: { length: number }) => edges.length > 0); \ No newline at end of file diff --git a/src/services/warp.ts b/src/services/warp.ts deleted file mode 100644 index e69de29..0000000 From bcc739e9b5291e08a8914a02aa383eeb588f00eb Mon Sep 17 00:00:00 2001 From: Jack Frain Date: Wed, 6 Nov 2024 16:25:47 -0500 Subject: [PATCH 2/3] show default spec on create --- src/pages/form/index.tsx | 7 ++++--- src/pages/form/service.ts | 2 +- src/pages/home/index.tsx | 1 - src/pages/show/index.tsx | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pages/form/index.tsx b/src/pages/form/index.tsx index 0f245b9..5e779aa 100644 --- a/src/pages/form/index.tsx +++ b/src/pages/form/index.tsx @@ -122,10 +122,11 @@ const EditorComponent: preact.FunctionComponent = ({ tx }) => { }; useEffect(() => { - if (current === "ready" && !loaded && context.spec && context.spec[0]) { + console.log(2, { current, loaded, context }) + if (current === "ready" && !loaded && context.spec) { setLoaded(true); - const specData = context.spec[0]; + const specData = context.spec; setSpecMeta({ Title: specData.Title, @@ -138,7 +139,7 @@ const EditorComponent: preact.FunctionComponent = ({ tx }) => { }); if (editor && editor.value() === "") { - editor.value(specData.html) + editor.value(specData.body) } } diff --git a/src/pages/form/service.ts b/src/pages/form/service.ts index 79150e8..a3e4207 100644 --- a/src/pages/form/service.ts +++ b/src/pages/form/service.ts @@ -33,7 +33,7 @@ const machine = createMachine({ "done", "ready", reduce((ctx: FormMachineContext, ev: FormMachineEvent) => { - return { ...ctx, spec: ev.data } + return { ...ctx, spec: ev.data[0] ? { ...ev.data[0], body: ev.data[0].html } : ev.data } }), ), ), diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 945b6b1..a56e5f7 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -154,7 +154,6 @@ const HomePage = () => { diff --git a/src/pages/form/service.ts b/src/pages/form/service.ts index a3e4207..2f46a00 100644 --- a/src/pages/form/service.ts +++ b/src/pages/form/service.ts @@ -10,6 +10,7 @@ import yaml from 'js-yaml' import services from "../../services" import Api from "../../lib" import { FormMachineContext, FormMachineCurrent, FormMachineEvent, FormMachineSend } from "./types" +import { ZodError } from "zod" const api = Api.init(services) @@ -60,17 +61,22 @@ const machine = createMachine({ ${ctx.md}`, ) - // add saved doc to local cache -- hold for now... - //.map(({ id }) => (cache.update(assoc(id, ctx.md)), { id })) - .map(({ id }) => ({ ...ctx, id })) .toPromise() }, - transition("done", "confirm"), + transition( + "done", + "confirm", + reduce((ctx: FormMachineContext, ev: FormMachineEvent) => { + const { txId } = ev.data as { txId?: string } + return { ...ctx, txId } + }), + ), transition( "error", "ready", reduce((ctx: FormMachineContext, ev: FormMachineEvent) => { - return { ...ctx, error: ev.error } + const { error } = ev.data as { error?: string } + return { ...ctx, saveError: error } }), ), ), diff --git a/src/pages/form/types.ts b/src/pages/form/types.ts index 39aca7c..047b4e7 100644 --- a/src/pages/form/types.ts +++ b/src/pages/form/types.ts @@ -4,19 +4,22 @@ import { ZodError } from "zod" export interface FormMachineContext { type?: string tx?: string | null + txId?: string | null spec?: FormSpec md?: string metadata?: Metadata error?: ZodError + saveError?: string id?: string } export interface FormMachineEvent { type: string md?: string - data?: FormSpec + data?: FormSpec | { txId?: string, error?: string } metadata?: Metadata error?: ZodError + saveError?: string [key: string]: unknown } diff --git a/src/pages/home/service.ts b/src/pages/home/service.ts index 70ebb0f..1d79bb3 100644 --- a/src/pages/home/service.ts +++ b/src/pages/home/service.ts @@ -50,7 +50,6 @@ const machine = createMachine({ transition( "done", "view", - // TODO: fix this type when stamping is working reduce((ctx: HomeMachineContext, ev: { data: number }) => { const specs = ctx.specs.map((s) => s.id === ctx.selected.id ? assoc("stamps", ev.data, s) : s, diff --git a/src/services/ao.ts b/src/services/ao.ts index a9855cb..dadd3e9 100644 --- a/src/services/ao.ts +++ b/src/services/ao.ts @@ -10,6 +10,7 @@ const VOUCHER_WHITELIST = [ "3y0YE11i21hpP8UY0Z1AVhtPoJD4V_AbEBx-g0j9wRc", // Vouch-wAR-Stake ] const MIN_VOUCH_SCORE = 2 +const getFirstMessage = compose(head, propOr([], 'Messages')) export const upload = async (md: { data: string tags: { @@ -17,9 +18,10 @@ export const upload = async (md: { value: string }[] }) => { - const getTag = (n: string): string | undefined => - md.tags.find(tag => tag.name === n)?.value + const getTag = (tags: Array<{ name: string, value: string }>) => (n: string): string | undefined => + tags.find(tag => tag.name === n)?.value + const getMetadataTag = getTag(md.tags) const args = { process: SPEC_PID, tags: [ @@ -29,56 +31,62 @@ export const upload = async (md: { }, { name: "Spec-DataProtocol", - value: getTag('Data-Protocol') + value: getMetadataTag('Data-Protocol') }, { name: "Spec-GroupId", - value: getTag('GroupId') + value: getMetadataTag('GroupId') }, { name: "Spec-Variant", - value: getTag('Variant') + value: getMetadataTag('Variant') }, { name: "Spec-Title", - value: getTag('Title') + value: getMetadataTag('Title') }, { name: "Spec-Description", - value: getTag('Description') + value: getMetadataTag('Description') }, { name: "Spec-Topics", - value: getTag('Topics').toString() + value: getMetadataTag('Topics').toString() }, { name: "Spec-Authors", - value: getTag('Authors').toString() + value: getMetadataTag('Authors').toString() }, { name: "Spec-Type", - value: getTag('Type') + value: getMetadataTag('Type') }, { name: "Spec-Forks", - value: getTag('Forks') + value: getMetadataTag('Forks') }, { name: "Spec-Content-Type", - value: getTag('Content-Type') + value: getMetadataTag('Content-Type') }, { name: "Spec-Render-With", - value: getTag('Render-With') + value: getMetadataTag('Render-With') }, ], data: md.data, signer: createDataItemSigner(window.arweaveWallet) } - const result = await message(args) + const messageId = await message(args) + const messageResult = await result({ process: SPEC_PID, message: messageId }) + const tags = compose( + propOr([], 'Tags'), + getFirstMessage + )(messageResult) as Array<{ name: string, value: string }> - return result + const getResultTag = getTag(tags) + return { status: getResultTag('Result'), error: getResultTag('Error'), txId: getResultTag('ID') } } export const query = async (tx: string) => { @@ -165,8 +173,7 @@ export const isVouched = async (tx: string) => { propOr({}, 'Vouchers'), JSON.parse, propOr('{}', 'Data'), - head, - propOr([], 'Messages') + getFirstMessage )(messageResult) let vouchScore = 0 @@ -177,6 +184,7 @@ export const isVouched = async (tx: string) => { if (vouchFor != tx) { throw new Error('Vouch has Vouch-For mismatch') } + // The value is a string like "4.5-USD" or "5-USD". We want to match the number. const valueStr = vouch['Value'].match(/^(\d+\.\d+)|(\d+)/g)?.[0] ?? "0" const value = parseFloat(valueStr) if (valueStr == null || value == null) { @@ -185,5 +193,5 @@ export const isVouched = async (tx: string) => { vouchScore += value } } - return { addr: tx, vouched: Boolean(vouchScore > MIN_VOUCH_SCORE) } + return { addr: tx, vouched: vouchScore > MIN_VOUCH_SCORE } } \ No newline at end of file diff --git a/src/services/stamps.ts b/src/services/stamps.ts index 6a7d073..95cc6a9 100644 --- a/src/services/stamps.ts +++ b/src/services/stamps.ts @@ -9,16 +9,17 @@ export const stampCounts = (txs: string[]) => { export const stamp = (tx: string) => { return stamps.hasStamped(tx).then((s) => { - return !s - ? stamps - .stamp(tx) - .then(() => new Promise((r) => setTimeout(r, 500))) - .then(() => stamps.count(tx).then(prop("vouched"))) - : Promise.reject("Already Stamped!") + if (s) { + return Promise.reject('Already Stamped!') } - ) + const addOne = (n: number) => n + 1 + const count = stampCount(tx).then(addOne) + return stamps + .stamp(tx) + .then(() => count) + }) } -export const stampCount = (tx) => { +export const stampCount = (tx: string) => { return stamps.count(tx).then(prop("vouched")); }