Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion frontend/messages/en-us.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,47 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from en-us!"
"hello_world": "Hello, {name} from en-us!",
"publish": {
"title": "Publish Article",
"subtitle": "Share your thoughts with the world",
"field_title": "Title",
"field_title_placeholder": "Enter article title",
"field_summary": "Summary",
"field_summary_placeholder": "Brief summary of your article",
"field_category": "Category (comma-separated)",
"field_category_placeholder": "e.g. Technology, Design, Business",
"field_category_id": "Category ID (Contract)",
"field_category_id_placeholder": "0",
"field_category_id_help": "The numeric ID for this article's category on the blockchain",
"field_author": "Author",
"field_author_placeholder": "Your name or pen name (optional)",
"field_author_help": "For repost scenarios; leave empty if you are the original author",
"field_royalty": "Royalty Percentage",
"field_royalty_help": "Basis points (100 = 1%, max 10000 = 100%)",
"field_cover_image": "Cover Image",
"field_cover_image_help": "PNG, JPG, GIF up to 10MB",
"field_cover_image_upload": "Upload cover image",
"field_cover_image_remove": "Remove image",
"field_content": "Content (Markdown supported)",
"field_content_placeholder": "Write your article content here...",
"field_postscript": "Postscript",
"field_postscript_placeholder": "Optional postscript or footer note...",
"button_publish": "Publish Article",
"button_clear": "Clear",
"required_fields": "* Required fields",
"status_validating": "Validating article...",
"status_uploading_cover": "Uploading cover image...",
"status_uploading_article": "Uploading article to Arweave...",
"status_publishing": "Publishing to blockchain...",
"status_success": "✨ Article published successfully!\n\nArweave: {arweaveId}\nTransaction: {txHash}",
"status_error": "Failed to publish: {error}",
"error_title_required": "Title is required",
"error_summary_required": "Summary is required",
"error_content_required": "Content is required",
"error_category_invalid": "Please select a valid category",
"error_royalty_invalid": "Royalty must be between 0-100%",
"error_image_invalid": "Please select a valid image file",
"error_image_too_large": "Image must be smaller than 10MB",
"error_publish": "Failed to publish: {error}"
}
}
45 changes: 44 additions & 1 deletion frontend/messages/zh-cn.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,47 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from zh-cn!"
"hello_world": "Hello, {name} from zh-cn!",
"publish": {
"title": "发布文章",
"subtitle": "与世界分享你的想法",
"field_title": "标题",
"field_title_placeholder": "请输入文章标题",
"field_summary": "摘要",
"field_summary_placeholder": "简要描述你的文章内容",
"field_category": "分类(用逗号分隔)",
"field_category_placeholder": "例如 技术, 设计, 商业",
"field_category_id": "分类 ID(合约)",
"field_category_id_placeholder": "0",
"field_category_id_help": "该文章在区块链上的分类数字ID",
"field_author": "作者",
"field_author_placeholder": "你的名字或笔名(可选)",
"field_author_help": "用于转载场景;如果你是原作者请留空",
"field_royalty": "版税百分比",
"field_royalty_help": "基点单位(100 = 1%,最大 10000 = 100%)",
"field_cover_image": "封面图片",
"field_cover_image_help": "PNG、JPG、GIF 格式,最大 10MB",
"field_cover_image_upload": "上传封面图片",
"field_cover_image_remove": "移除图片",
"field_content": "内容(支持 Markdown)",
"field_content_placeholder": "在此输入你的文章内容...",
"field_postscript": "附言",
"field_postscript_placeholder": "可选的附言或页脚说明...",
"button_publish": "发布文章",
"button_clear": "清空",
"required_fields": "* 必填项",
"status_validating": "验证文章...",
"status_uploading_cover": "上传封面图片...",
"status_uploading_article": "上传文章到 Arweave...",
"status_publishing": "发布到区块链...",
"status_success": "✨ 文章发布成功!\n\nArweave: {arweaveId}\n交易哈希: {txHash}",
"status_error": "发布失败:{error}",
"error_title_required": "标题为必填项",
"error_summary_required": "摘要为必填项",
"error_content_required": "内容为必填项",
"error_category_invalid": "请选择有效的分类",
"error_royalty_invalid": "版税必须在 0-100% 之间",
"error_image_invalid": "请选择有效的图片文件",
"error_image_too_large": "图片文件必须小于 10MB",
"error_publish": "发布失败:{error}"
}
}
1 change: 1 addition & 0 deletions frontend/project.inlang/cache/plugins/2sy648wh9sugi

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions frontend/project.inlang/cache/plugins/ygx0uiahq6uw

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/project.inlang/project_id
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CPk3mRmsKsU62dkXUv
5 changes: 4 additions & 1 deletion frontend/src/lib/arweave/irys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ export async function createIrysUploader(config: IrysConfig = { network: 'devnet

// 使用类型断言解决 viem 版本与 @irys/web-upload-ethereum-viem-v2 的类型不兼容问题
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let builder = WebUploader(WebEthereum).withAdapter(ViemV2Adapter(walletClient as any, { publicClient: publicClient as any }))
const viemAdapter = ViemV2Adapter(walletClient as any, { publicClient: publicClient as any })

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let builder = (WebUploader as any)(WebEthereum).withAdapter(viemAdapter)

if (config.network === 'devnet') {
const rpcUrl = config.rpcUrl || DEFAULT_RPC_URL
Expand Down
92 changes: 92 additions & 0 deletions frontend/src/lib/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Smart contract interaction utilities
* Handles publishing articles to BlogHub contract
*/

import { createWalletClient, custom } from 'viem'
import { optimismSepolia } from 'viem/chains'

// BlogHub contract ABI (minimal for publish function)
const BLOGHUB_ABI = [
{
name: 'publish',
type: 'function',
inputs: [
{ name: '_arweaveId', type: 'string' },
{ name: '_categoryId', type: 'uint64' },
{ name: '_royaltyBps', type: 'uint96' },
{ name: '_originalAuthor', type: 'string' }
],
outputs: [{ type: 'uint256' }],
stateMutability: 'nonpayable'
}
] as const

// BlogHub contract address (Optimism Sepolia testnet)
// TODO: Replace with your actual contract address
const BLOGHUB_CONTRACT_ADDRESS = '0x' as `0x${string}`

/**
* Get wallet client for contract interaction
*/
async function getWalletClient() {
if (typeof window === 'undefined' || !window.ethereum) {
throw new Error('Ethereum provider not found. Please install MetaMask or another wallet.')
}

return createWalletClient({
chain: optimismSepolia,
transport: custom(window.ethereum)
})
}

/**
* Publish article to BlogHub contract
* @param arweaveId - Arweave hash of article content
* @param categoryId - Category ID (0-based)
* @param royaltyBps - Royalty basis points (0-10000, where 100 = 1%)
* @param originalAuthor - Original author name (optional, for repost scenarios)
* @returns Transaction hash
*/
export async function publishToContract(
arweaveId: string,
categoryId: bigint,
royaltyBps: bigint,
originalAuthor: string = ''
): Promise<string> {
if (!arweaveId) {
throw new Error('Arweave ID is required')
}

if (categoryId < 0n) {
throw new Error('Category ID must be non-negative')
}

if (royaltyBps > 10000n) {
throw new Error('Royalty percentage cannot exceed 100% (10000 basis points)')
}

if (originalAuthor && originalAuthor.length > 64) {
throw new Error('Original author name is too long (max 64 characters)')
}

try {
const walletClient = await getWalletClient()

// Call publish function
const txHash = await walletClient.writeContract({
address: BLOGHUB_CONTRACT_ADDRESS,
abi: BLOGHUB_ABI,
functionName: 'publish',
args: [arweaveId, categoryId, royaltyBps, originalAuthor]
})

console.log(`Article published to contract. Tx: ${txHash}`)
return txHash
} catch (error) {
console.error('Error publishing to contract:', error)
throw new Error(
`Failed to publish to contract: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
100 changes: 100 additions & 0 deletions frontend/src/lib/publish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Article publishing orchestration
* Coordinates upload to Arweave and publishing to blockchain
*/

import { uploadArticle, uploadImage } from './arweave/upload'
import { publishToContract } from './contracts'
import type { ArticleMetadata } from './arweave/types'

export interface PublishArticleParams {
title: string
summary: string
content: string
tags: string[]
coverImage: File | null
categoryId: bigint
royaltyBps: bigint
originalAuthor?: string
}

export interface PublishArticleResult {
arweaveId: string
txHash: string
articleId?: string
}

/**
* Publish article: upload to Arweave, then to blockchain
* @param params - Article publishing parameters
* @returns Arweave ID and transaction hash
*/
export async function publishArticle(params: PublishArticleParams): Promise<PublishArticleResult> {
const {
title,
summary,
content,
tags,
coverImage,
categoryId,
royaltyBps,
originalAuthor = ''
} = params

// Validation
if (!title.trim()) {
throw new Error('Title is required')
}

if (!content.trim()) {
throw new Error('Content is required')
}

if (!summary.trim()) {
throw new Error('Summary is required')
}

if (categoryId < 0n) {
throw new Error('Valid category is required')
}

if (royaltyBps < 0n || royaltyBps > 10000n) {
throw new Error('Royalty must be between 0 and 100% (0-10000 basis points)')
}

try {
// Step 1: Upload cover image if provided
let coverImageHash: string | undefined
if (coverImage) {
console.log('Step 1: Uploading cover image...')
coverImageHash = await uploadImage(coverImage, 'devnet')
console.log(`Cover image uploaded: ${coverImageHash}`)
}

// Step 2: Upload article to Arweave
console.log('Step 2: Uploading article to Arweave...')
const articleMetadata: Omit<ArticleMetadata, 'createdAt' | 'version'> = {
title: title.trim(),
summary: summary.trim(),
content: content.trim(),
coverImage: coverImageHash,
tags
}

const arweaveId = await uploadArticle(articleMetadata, 'devnet')
console.log(`Article uploaded to Arweave: ${arweaveId}`)

// Step 3: Publish to blockchain
console.log('Step 3: Publishing to blockchain...')
const txHash = await publishToContract(arweaveId, categoryId, royaltyBps, originalAuthor)
console.log(`Article published to blockchain: ${txHash}`)

return {
arweaveId,
txHash
}
} catch (error) {
console.error('Error during article publishing:', error)
throw error
}
}
Loading