Publish content with an atomic price and visibility policy

This commit is contained in:
archipelago
2026-10-06 07:51:00 -04:00
parent 65b51b98f4
commit 19207282f9
9 changed files with 456 additions and 142 deletions
+10 -18
View File
@@ -400,34 +400,26 @@ async function save() {
(i) => matchesThisFile(i.filename)
)?.id
// Add if not in catalog
const policy: Record<string, unknown> = { access: accessType.value, availability: 'all_peers' }
if (accessType.value === 'paid') {
policy.price_sats = priceSats.value
policy.accepted_methods = [...acceptedSet.value]
}
if (!itemId) {
const ext = props.filename.split('.').pop()?.toLowerCase() || ''
const addRes = await rpcClient.call<{ item: { id: string } }>({
method: 'content.add',
await rpcClient.call({
method: 'content.publish',
params: {
filename: (props.filepath || props.filename).replace(/^\/+/, ''),
mime_type: SHARE_MIME_MAP[ext] || 'application/octet-stream',
description: '',
...policy,
},
})
itemId = addRes.item.id
} else {
await rpcClient.call({ method: 'content.configure', params: { id: itemId, ...policy } })
}
// Set pricing
const pricingParams: Record<string, unknown> = { id: itemId, access: accessType.value }
if (accessType.value === 'paid') {
pricingParams.price_sats = priceSats.value
pricingParams.accepted_methods = [...acceptedSet.value]
}
await rpcClient.call({ method: 'content.set-pricing', params: pricingParams })
// Set availability to all peers
await rpcClient.call({
method: 'content.set-availability',
params: { id: itemId, availability: 'all_peers' },
})
const label =
accessType.value === 'paid'
? `Shared for ${priceSats.value} sats`
@@ -0,0 +1,25 @@
import { beforeEach, afterEach, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import ShareModal from '../ShareModal.vue'
import { rpcClient } from '@/api/rpc-client'
vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } }))
beforeEach(() => { vi.clearAllMocks(); vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) })
afterEach(() => { vi.clearAllTimers(); vi.useRealTimers() })
it.each([false, true])('publishes one complete policy for an existing share: %s', async existing => {
vi.mocked(rpcClient.call).mockImplementation(async ({ method }) => {
if (method === 'content.list-mine') return { items: existing ? [{ id: 'saved-id', filename: 'film.mp4', access: 'free' }] : [] }
return { item: { id: 'new-id' } }
})
const wrapper = mount(ShareModal, { props: { filename: 'film.mp4', filepath: '/film.mp4', isDir: false } })
await flushPromises()
const vm = (wrapper.vm as any).$.setupState
vm.shared = true; vm.accessType = 'paid'; vm.priceSats = 1; vm.acceptedSet = new Set(['ecash'])
await vm.save()
const writes = vi.mocked(rpcClient.call).mock.calls.map(([call]) => call).filter(call => ['content.publish','content.configure','content.set-pricing','content.set-availability'].includes(call.method))
expect(writes).toHaveLength(1)
expect(writes[0]?.method).toBe(existing ? 'content.configure' : 'content.publish')
expect(writes[0]?.params).toMatchObject({ access: 'paid', price_sats: 1, accepted_methods: ['ecash'], availability: 'all_peers' })
expect(writes[0]?.params).toMatchObject(existing ? { id: 'saved-id' } : { filename: 'film.mp4' })
wrapper.unmount()
})
+4 -15
View File
@@ -467,27 +467,16 @@ async function addContentItem() {
addContentError.value = ''
try {
await rpcClient.call({
method: 'content.add',
method: 'content.publish',
params: {
filename: newContentFilename.value.trim(),
mime_type: newContentMimeType.value.trim() || 'application/octet-stream',
description: newContentDescription.value.trim(),
access: newContentAccess.value,
availability: 'all_peers',
...(newContentAccess.value === 'paid' ? { price_sats: newContentPrice.value || 100 } : {}),
},
})
if (newContentAccess.value !== 'free') {
const items = (await rpcClient.call<{ items: ContentItemData[] }>({ method: 'content.list-mine' })).items || []
const latest = items[items.length - 1]
if (latest) {
await rpcClient.call({
method: 'content.set-pricing',
params: {
id: latest.id,
access: newContentAccess.value,
...(newContentAccess.value === 'paid' ? { price_sats: newContentPrice.value || 100 } : {}),
},
})
}
}
showAddContentModal.value = false
newContentFilename.value = ''
newContentMimeType.value = 'application/octet-stream'
@@ -116,3 +116,17 @@ describe('Web5SharedContent', () => {
expect(wrapper.text()).not.toContain('Refreshing peer content...')
})
})
it('creates a paid Web5 share with its price in the first write', async () => {
vi.mocked(rpcClient.call).mockReset()
vi.mocked(rpcClient.call).mockResolvedValue({ items: [] })
const wrapper = mount(Web5SharedContent, { props: { showStagger: false, peers: [] } })
await flushPromises()
const vm = (wrapper.vm as any).$.setupState
vm.newContentFilename = 'film.mp4'; vm.newContentAccess = 'paid'; vm.newContentPrice = 1
await vm.addContentItem()
const writes = vi.mocked(rpcClient.call).mock.calls.map(([call]) => call).filter(call => ['content.publish','content.set-pricing'].includes(call.method))
expect(writes).toHaveLength(1)
expect(writes[0]).toMatchObject({ method: 'content.publish', params: { filename: 'film.mp4', access: 'paid', price_sats: 1, availability: 'all_peers' } })
wrapper.unmount()
})