From 9efedd9f9efe66b58669330ea4ea55a7ca49b30d Mon Sep 17 00:00:00 2001
From: Akira Taguchi <31825085+lambdakilo@users.noreply.github.com>
Date: Wed, 12 Aug 2026 14:30:15 +0300
Subject: [PATCH 1/4] feat: bounty creation nostr
---
src/lib/bounty.test.ts | 89 ++++++++++++
src/lib/bounty.ts | 54 ++++++++
src/lib/index.ts | 1 +
src/routes/bounties/new/+page.svelte | 198 ++++++++++++++++++---------
4 files changed, 276 insertions(+), 66 deletions(-)
create mode 100644 src/lib/bounty.test.ts
create mode 100644 src/lib/bounty.ts
diff --git a/src/lib/bounty.test.ts b/src/lib/bounty.test.ts
new file mode 100644
index 0000000..07493e6
--- /dev/null
+++ b/src/lib/bounty.test.ts
@@ -0,0 +1,89 @@
+import { describe, it, expect, vi } from 'vitest';
+import type { BountyDraft } from './bounty';
+
+// ---------------------------------------------------------------------------
+// Mock the ndk singleton so no real WebSocket connections are made.
+// ---------------------------------------------------------------------------
+
+vi.mock('./ndk', () => ({
+ ndk: () => ({})
+}));
+
+// Import AFTER the mock is registered
+const { buildBountyEvent } = await import('./bounty');
+
+// ---------------------------------------------------------------------------
+
+function makeDraft(overrides: Partial = {}): BountyDraft {
+ return {
+ title: 'Fix memory leak',
+ description: 'Find and fix the leak.',
+ amountSats: 250000,
+ deadline: '2026-08-20',
+ topics: '',
+ ...overrides
+ };
+}
+
+function tagValue(tags: string[][], name: string): string | undefined {
+ return tags.find((tag) => tag[0] === name)?.[1];
+}
+
+describe('buildBountyEvent', () => {
+ it('builds a kind-30050 event with the trimmed description as content', () => {
+ const event = buildBountyEvent(
+ makeDraft({ description: ' Find and fix the leak. ' })
+ );
+
+ expect(event.kind).toBe(30050);
+ expect(event.content).toBe('Find and fix the leak.');
+ });
+
+ it('uses a fresh UUID as the d tag on every call', () => {
+ const first = tagValue(buildBountyEvent(makeDraft()).tags, 'd');
+ const second = tagValue(buildBountyEvent(makeDraft()).tags, 'd');
+
+ const uuid =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
+ expect(first).toMatch(uuid);
+ expect(second).toMatch(uuid);
+ expect(first).not.toBe(second);
+ });
+
+ it('sets title (trimmed), amount_sats, s and resolution_mode tags', () => {
+ const { tags } = buildBountyEvent(
+ makeDraft({ title: ' Fix memory leak ', amountSats: 21000 })
+ );
+
+ expect(tagValue(tags, 'title')).toBe('Fix memory leak');
+ expect(tagValue(tags, 'amount_sats')).toBe('21000');
+ expect(tagValue(tags, 's')).toBe('open');
+ expect(tagValue(tags, 'resolution_mode')).toBe('A');
+ });
+
+ it('sets bounty_deadline to 23:59:59 local time of the picked date', () => {
+ const { tags } = buildBountyEvent(makeDraft({ deadline: '2026-08-20' }));
+
+ const endOfDay = new Date(2026, 7, 20, 23, 59, 59);
+ expect(tagValue(tags, 'bounty_deadline')).toBe(
+ String(Math.floor(endOfDay.getTime() / 1000))
+ );
+ });
+
+ it('lowercases, trims and dedupes topics into t tags', () => {
+ const { tags } = buildBountyEvent(
+ makeDraft({ topics: 'TypeScript, nostr,, NOSTR , ' })
+ );
+
+ expect(tags.filter((tag) => tag[0] === 't')).toEqual([
+ ['t', 'typescript'],
+ ['t', 'nostr']
+ ]);
+ });
+
+ it('emits no t tags when topics is empty', () => {
+ const { tags } = buildBountyEvent(makeDraft({ topics: '' }));
+
+ expect(tags.filter((tag) => tag[0] === 't')).toEqual([]);
+ });
+});
diff --git a/src/lib/bounty.ts b/src/lib/bounty.ts
new file mode 100644
index 0000000..d652a8c
--- /dev/null
+++ b/src/lib/bounty.ts
@@ -0,0 +1,54 @@
+import { NDKEvent } from '@nostr-dev-kit/ndk';
+import { ndk } from './ndk';
+
+export const BOUNTY_KIND = 30050;
+
+export interface BountyDraft {
+ title: string;
+ description: string; // markdown
+ amountSats: number;
+ deadline: string; // YYYY-MM-DD, user's local time zone
+ topics: string; // comma-separated, may be empty
+}
+
+export function buildBountyEvent(draft: BountyDraft): {
+ kind: number;
+ content: string;
+ tags: string[][];
+} {
+ // End of the picked day (23:59:59) in the poster's local time zone —
+ // new Date('YYYY-MM-DD') would parse as UTC midnight and shift the day.
+ const [year, month, day] = draft.deadline.split('-').map(Number);
+ const deadlineUnix = Math.floor(
+ new Date(year, month - 1, day, 23, 59, 59).getTime() / 1000
+ );
+ const topics = [
+ ...new Set(
+ draft.topics
+ .split(',')
+ .map((topic) => topic.trim().toLowerCase())
+ .filter(Boolean)
+ )
+ ];
+ return {
+ kind: BOUNTY_KIND,
+ content: draft.description.trim(),
+ tags: [
+ ['d', crypto.randomUUID()],
+ ['title', draft.title.trim()],
+ ['amount_sats', String(draft.amountSats)],
+ ['s', 'open'],
+ // Mode B (oracle) requires infrastructure that doesn't exist yet.
+ ['resolution_mode', 'A'],
+ ['bounty_deadline', String(deadlineUnix)],
+ ...topics.map((topic) => ['t', topic])
+ ]
+ };
+}
+
+export async function publishBounty(draft: BountyDraft): Promise {
+ const event = new NDKEvent(ndk(), buildBountyEvent(draft));
+ await event.sign(); // NIP-07 extension prompt (signer set at login)
+ await event.publish(); // throws NDKPublishError if no relay accepts
+ return event;
+}
diff --git a/src/lib/index.ts b/src/lib/index.ts
index ca30e6b..9208b19 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -1,3 +1,4 @@
export * from './ndk';
export * from './nostr';
export * from './auth.svelte';
+export * from './bounty';
diff --git a/src/routes/bounties/new/+page.svelte b/src/routes/bounties/new/+page.svelte
index 7320b49..cadf59b 100644
--- a/src/routes/bounties/new/+page.svelte
+++ b/src/routes/bounties/new/+page.svelte
@@ -1,10 +1,19 @@
@@ -48,80 +83,111 @@
Post a software bounty to Nostr, paid in Bitcoin.
-
+ {/if}
From 3ac4340b7052c237702c46b55c4395a40efa3025 Mon Sep 17 00:00:00 2001
From: guildm4ster <289806744+guildm4ster@users.noreply.github.com>
Date: Thu, 13 Aug 2026 13:41:24 +0300
Subject: [PATCH 2/4] feat: update placeholders in bounty creation form
---
src/routes/bounties/new/+page.svelte | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/routes/bounties/new/+page.svelte b/src/routes/bounties/new/+page.svelte
index cadf59b..dd9d95a 100644
--- a/src/routes/bounties/new/+page.svelte
+++ b/src/routes/bounties/new/+page.svelte
@@ -116,7 +116,7 @@
bind:value={title}
maxlength="120"
required
- placeholder="Fix memory leak in Rust Bitcoin parser"
+ placeholder="Short title for the bounty"
class={inputClasses}
/>
@@ -127,7 +127,7 @@
bind:value={description}
rows="6"
required
- placeholder="What needs to be done, and what does success look like?"
+ placeholder="What needs to be done, and what are the exact bounty acceptance criteria?"
class={inputClasses}>
Markdown supported
@@ -147,7 +147,7 @@