From 032c32ad7d5e9202ce69a4ed466663acb0e6654f Mon Sep 17 00:00:00 2001 From: mgarbs Date: Tue, 7 Jan 2025 09:21:02 -0500 Subject: [PATCH 01/12] Remove line setting github token --- _includes/head.html | 1 - 1 file changed, 1 deletion(-) diff --git a/_includes/head.html b/_includes/head.html index 2efbe7cb2..4e6cffa83 100644 --- a/_includes/head.html +++ b/_includes/head.html @@ -1,5 +1,4 @@ - From 2d0dcd56ed90560fc1848a5aace77a6e0ed921db Mon Sep 17 00:00:00 2001 From: mgarbs Date: Tue, 7 Jan 2025 09:21:44 -0500 Subject: [PATCH 02/12] Add workflow to fetch hip data from prs --- .github/workflows/update-draft-hips.yml | 118 ++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .github/workflows/update-draft-hips.yml diff --git a/.github/workflows/update-draft-hips.yml b/.github/workflows/update-draft-hips.yml new file mode 100644 index 000000000..c1d99bc26 --- /dev/null +++ b/.github/workflows/update-draft-hips.yml @@ -0,0 +1,118 @@ +name: Update Draft HIPs Data + +on: + schedule: + - cron: "0 */6 * * *" # Runs every 6 hours + workflow_dispatch: # Allows manual triggering + +jobs: + update-draft-hips: + if: github.ref == 'refs/heads/main' # Only run on main branch + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Create Script + run: | + mkdir -p _data + cat << 'EOF' > fetch-draft-hips.js + const https = require('https'); + + async function makeGraphQLRequest(query, token) { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: '/graphql', + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'Node.js' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', () => resolve(JSON.parse(data))); + }); + + req.on('error', reject); + req.write(JSON.stringify({ query })); + req.end(); + }); + } + + async function getAllPRs() { + const query = ` + query { + repository(name: "hedera-improvement-proposal", owner: "hashgraph") { + pullRequests(first: 100, states: [OPEN], orderBy: {field: CREATED_AT, direction: DESC}) { + nodes { + title + number + url + headRefOid + files(first: 100) { + edges { + node { + path + additions + deletions + } + } + } + author { + login + } + } + } + } + } + `; + + try { + const result = await makeGraphQLRequest(query, process.env.GITHUB_TOKEN); + + if (result.errors) { + console.error('GraphQL errors:', result.errors); + process.exit(1); + } + + return result.data.repository.pullRequests.nodes; + } catch (error) { + console.error('Error fetching PRs:', error); + throw error; + } + } + + // Run the main function + getAllPRs().then(prs => { + const fs = require('fs'); + fs.writeFileSync('_data/draft_hips.json', JSON.stringify(prs, null, 2)); + }).catch(error => { + console.error('Failed to fetch PRs:', error); + process.exit(1); + }); + EOF + + - name: Run Script + run: node fetch-draft-hips.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Commit and Push Changes + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add _data/draft_hips.json + git commit -m "Update draft HIPs data [skip ci]" || echo "No changes to commit" + git push origin main || echo "No changes to push" From f3bc83bb2c65dff8fa11b604a6c3067db0dbd319 Mon Sep 17 00:00:00 2001 From: mgarbs Date: Tue, 7 Jan 2025 09:22:47 -0500 Subject: [PATCH 03/12] Add query result file to be included in build --- _config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/_config.yml b/_config.yml index 770718471..a15d7d38d 100644 --- a/_config.yml +++ b/_config.yml @@ -32,6 +32,9 @@ defaults: values: layout: "hip" +include: + - _data + # Exclude from processing. # The following items will not be processed, by default. Create a custom list # to override the default setting. From 1aadcc4f89e06e0e76e5af86aba12ba56d3cec27 Mon Sep 17 00:00:00 2001 From: mgarbs Date: Tue, 7 Jan 2025 09:23:32 -0500 Subject: [PATCH 04/12] Update script to fetch workflow json result --- assets/js/pr-integration.js | 69 +++++++++---------------------------- 1 file changed, 16 insertions(+), 53 deletions(-) diff --git a/assets/js/pr-integration.js b/assets/js/pr-integration.js index 06e171141..121f97b6d 100644 --- a/assets/js/pr-integration.js +++ b/assets/js/pr-integration.js @@ -69,51 +69,14 @@ class HIPPRIntegration { async fetchPRData() { try { - const token = document.querySelector('meta[name="github-token"]').content; - - const query = { - query: `query { - repository(name: "hedera-improvement-proposal", owner: "hashgraph") { - pullRequests(first: 100, orderBy: {field: CREATED_AT, direction: DESC}, states: [OPEN]) { - nodes { - title - number - url - headRefOid - files(last: 100) { - edges { - node { - path - additions - deletions - } - } - } - author { - login - } - } - } - } - }` - }; - - const response = await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(query) - }); + // Try with site.baseurl if it's set in _config.yml + const baseUrl = document.querySelector('meta[name="site-baseurl"]')?.content || ''; + const response = await fetch(`${baseUrl}/_data/draft_hips.json`); - const data = await response.json(); - - if (!response.ok || data.errors) { - console.error('GraphQL errors:', data.errors); - return null; + if (!response.ok) { + throw new Error('Failed to fetch draft HIPs data'); } - return data.data.repository.pullRequests.nodes; + return await response.json(); } catch (error) { console.error('Error in fetchPRData:', error); throw error; @@ -123,14 +86,14 @@ class HIPPRIntegration { async filterHIPPRs(prs) { const validHips = []; const seenPRs = new Set(); - + for (const pr of prs) { if (seenPRs.has(pr.number)) continue; - + const mdFiles = pr.files.edges.filter(file => file.node.path.endsWith('.md')); let bestMetadata = null; let bestFile = null; - + // Try all MD files and pick the one with the most complete metadata for (const file of mdFiles) { try { @@ -138,16 +101,16 @@ class HIPPRIntegration { const response = await fetch(contentUrl); const content = await response.text(); const metadata = this.parseHIPMetadata(content); - + // Skip template files and empty metadata if (file.node.path.includes('template') || !metadata.title) { continue; } - + // If we don't have metadata yet, or this file has a better title - if (!bestMetadata || - (metadata.title && metadata.title.length > 3 && - (!bestMetadata.title || metadata.title.length > bestMetadata.title.length))) { + if (!bestMetadata || + (metadata.title && metadata.title.length > 3 && + (!bestMetadata.title || metadata.title.length > bestMetadata.title.length))) { bestMetadata = metadata; bestFile = file; } @@ -155,7 +118,7 @@ class HIPPRIntegration { console.error(`Error checking file ${file.node.path}:`, error); } } - + // If we found valid metadata, add it to the results if (bestMetadata && bestFile) { validHips.push({ @@ -166,7 +129,7 @@ class HIPPRIntegration { seenPRs.add(pr.number); } } - + return validHips; } From e656f2a9c7086059e5c4cca66dbf13a3ace5772b Mon Sep 17 00:00:00 2001 From: Michael Garber Date: Fri, 10 Jan 2025 10:06:20 -0500 Subject: [PATCH 05/12] Update update-draft-hips.yml to use hardened SHA versions Signed-off-by: Michael Garber --- .github/workflows/update-draft-hips.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-draft-hips.yml b/.github/workflows/update-draft-hips.yml index c1d99bc26..78ad70bd1 100644 --- a/.github/workflows/update-draft-hips.yml +++ b/.github/workflows/update-draft-hips.yml @@ -13,10 +13,10 @@ jobs: contents: write pull-requests: read steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version: "18" From 80653709261d538bdf71a854c878f119610b3ac7 Mon Sep 17 00:00:00 2001 From: Michael Garber Date: Mon, 13 Jan 2025 18:15:29 -0500 Subject: [PATCH 06/12] Update update-draft-hips.yml bump node version to 20 Signed-off-by: Michael Garber --- .github/workflows/update-draft-hips.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-draft-hips.yml b/.github/workflows/update-draft-hips.yml index 78ad70bd1..962c2d518 100644 --- a/.github/workflows/update-draft-hips.yml +++ b/.github/workflows/update-draft-hips.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: - node-version: "18" + node-version: "20" - name: Create Script run: | From a596983bba4f6bacf8f0bfea5d73bcbc84612220 Mon Sep 17 00:00:00 2001 From: Michael Garber Date: Thu, 2 Jan 2025 10:51:00 -0500 Subject: [PATCH 07/12] Display pr draft hips (#1091) Co-authored-by: mgarbs Signed-off-by: mgarbs --- _includes/head.html | 1 + _includes/hipstable.md | 136 ++++++++++----- _layouts/default.html | 2 + assets/js/pr-integration.js | 326 ++++++++++++++++++++++++++++++++++++ 4 files changed, 424 insertions(+), 41 deletions(-) create mode 100644 assets/js/pr-integration.js diff --git a/_includes/head.html b/_includes/head.html index 4e6cffa83..2efbe7cb2 100644 --- a/_includes/head.html +++ b/_includes/head.html @@ -1,4 +1,5 @@ + diff --git a/_includes/hipstable.md b/_includes/hipstable.md index 0e796ca0f..1ec42ee37 100644 --- a/_includes/hipstable.md +++ b/_includes/hipstable.md @@ -10,9 +10,11 @@ +

Status     

+

Council Approval     

- - + +
- -{% for status in site.data.statuses %} -{% assign hips = include.hips | where: "status", status | where: "category", category | where: "type", type | sort: "hip" | reverse %} -{% assign count = hips.size %} -{% if count > 0 %} -

{{ status }}

- + + + + +

Draft

+
- {% if status == "Last Call" %} - - {% else %} - - {% endif %} - - {% for page in hips %} - - - - - - {% if status == "Last Call" %} - - {% else %} - {% if page.category == "Mirror" %} - - {% else %} - - {% endif %} - {% endif %} - - {% endfor %} - +
Number Title Author Needs Council ApprovalReview Period EndsRelease
{{ page.hip | xml_escape }}{{ page.title | xml_escape }}{% include authorslist.html authors=page.author %} - {% if page.needs-council-approval %} - Yes - {% else %} - No - {% endif %} - {{ page.last-call-date-time | date_to_rfc822 }}{{page.release|xml_escape}}{{page.release|xml_escape}}
-{% endif %} -{% endfor %} + + +{% for status in site.data.statuses %} + {% assign hips = include.hips | where: "status", status | where: "category", category | where: "type", type | sort: "hip" | reverse %} + {% assign count = hips.size %} + {% if count > 0 %} +

+ {{ status | capitalize }} + +

+ + + + + + + + + {% if status == "Last Call" %} + + {% else %} + + {% endif %} + + + + {% for page in hips %} + + + + + + + + + + + {% if status == "Last Call" %} + + {% else %} + {% if page.category == "Mirror" %} + + {% else %} + + {% endif %} + {% endif %} + + {% endfor %} + +
NumberTitleAuthorNeeds Council ApprovalReview Period EndsRelease
+ {{ page.hip | xml_escape }} + + {{ page.title | xml_escape }} + + {% include authorslist.html authors=page.author %} + + {% if page.needs-council-approval %} + Yes + {% else %} + No + {% endif %} + + {{ page.last-call-date-time | date_to_rfc822 }} + + + {{page.release|xml_escape}} + + + + {{page.release|xml_escape}} + +
+ {% endif %} +{% endfor %} \ No newline at end of file diff --git a/_layouts/default.html b/_layouts/default.html index 1a00c357d..5ba896c89 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -13,9 +13,11 @@ {%- include footer.html -%} + + diff --git a/assets/js/pr-integration.js b/assets/js/pr-integration.js new file mode 100644 index 000000000..06e171141 --- /dev/null +++ b/assets/js/pr-integration.js @@ -0,0 +1,326 @@ +class HIPPRIntegration { + constructor() { + this.initialize(); + this.setupStyles(); + } + + setupStyles() { + const styles = ` + .hip-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + } + .hip-modal-content { + background: white; + padding: 20px; + border-radius: 8px; + max-width: 80%; + max-height: 80vh; + overflow-y: auto; + position: relative; + } + .hip-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + border-bottom: 1px solid #eee; + padding-bottom: 10px; + } + .close-button { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + } + .hip-modal-body { + line-height: 1.6; + } + .hip-modal-body img { + max-width: 100%; + } + `; + const styleSheet = document.createElement('style'); + styleSheet.textContent = styles; + document.head.appendChild(styleSheet); + } + + async initialize() { + try { + const prData = await this.fetchPRData(); + if (prData) { + const validHips = await this.filterHIPPRs(prData); + if (validHips.length > 0) { + this.addHIPsToTable(validHips); + } + } + } catch (error) { + console.error('Failed to initialize PR integration:', error); + } + } + + async fetchPRData() { + try { + const token = document.querySelector('meta[name="github-token"]').content; + + const query = { + query: `query { + repository(name: "hedera-improvement-proposal", owner: "hashgraph") { + pullRequests(first: 100, orderBy: {field: CREATED_AT, direction: DESC}, states: [OPEN]) { + nodes { + title + number + url + headRefOid + files(last: 100) { + edges { + node { + path + additions + deletions + } + } + } + author { + login + } + } + } + } + }` + }; + + const response = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(query) + }); + + const data = await response.json(); + + if (!response.ok || data.errors) { + console.error('GraphQL errors:', data.errors); + return null; + } + return data.data.repository.pullRequests.nodes; + } catch (error) { + console.error('Error in fetchPRData:', error); + throw error; + } + } + + async filterHIPPRs(prs) { + const validHips = []; + const seenPRs = new Set(); + + for (const pr of prs) { + if (seenPRs.has(pr.number)) continue; + + const mdFiles = pr.files.edges.filter(file => file.node.path.endsWith('.md')); + let bestMetadata = null; + let bestFile = null; + + // Try all MD files and pick the one with the most complete metadata + for (const file of mdFiles) { + try { + const contentUrl = `https://raw.githubusercontent.com/hashgraph/hedera-improvement-proposal/${pr.headRefOid}/${file.node.path}`; + const response = await fetch(contentUrl); + const content = await response.text(); + const metadata = this.parseHIPMetadata(content); + + // Skip template files and empty metadata + if (file.node.path.includes('template') || !metadata.title) { + continue; + } + + // If we don't have metadata yet, or this file has a better title + if (!bestMetadata || + (metadata.title && metadata.title.length > 3 && + (!bestMetadata.title || metadata.title.length > bestMetadata.title.length))) { + bestMetadata = metadata; + bestFile = file; + } + } catch (error) { + console.error(`Error checking file ${file.node.path}:`, error); + } + } + + // If we found valid metadata, add it to the results + if (bestMetadata && bestFile) { + validHips.push({ + pr, + metadata: bestMetadata, + filePath: bestFile.node.path + }); + seenPRs.add(pr.number); + } + } + + return validHips; + } + + checkForNewHipFormat(content) { + const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (!frontmatterMatch) return false; + + const frontmatter = frontmatterMatch[1].toLowerCase(); + const requiredPatterns = [ + /\btitle\s*:/, + /\bauthor\s*:/, + /\bcategory\s*:/, + /\bcreated\s*:/ + ]; + + return requiredPatterns.every(pattern => pattern.test(frontmatter)); + } + + isValidHIPContent(metadata) { + const essentialFields = ['title', 'type']; + const hasEssentialFields = essentialFields.every(field => metadata[field]); + const desiredFields = ['hip', 'author', 'status', 'created']; + const desiredFieldCount = desiredFields.filter(field => metadata[field]).length; + return hasEssentialFields && desiredFieldCount >= 2; + } + + parseHIPMetadata(content) { + const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (!frontmatterMatch) return {}; + + const metadata = {}; + const lines = frontmatterMatch[1].split('\n'); + + for (const line of lines) { + const [key, ...valueParts] = line.split(':'); + if (key && valueParts.length) { + const value = valueParts.join(':').trim(); + metadata[key.trim().toLowerCase()] = value; + } + } + + return metadata; + } + + addHIPsToTable(hips) { + const wrapper = document.querySelector('main .wrapper'); + if (!wrapper) return; + + const lastStatusSection = wrapper.lastElementChild; + const draftContainer = document.createElement('div'); + draftContainer.innerHTML = ` +

Draft

+ + + + + + + + + + +
NumberTitleAuthorNeeds Council Approval
+ `; + + wrapper.insertBefore(draftContainer, lastStatusSection.nextSibling); + const tbody = draftContainer.querySelector('tbody'); + const table = draftContainer.querySelector('.hipstable'); + + hips.forEach(({ pr, metadata }) => { + if (!metadata.title || metadata.title.trim() === '') return; + + const needsApproval = String(metadata['needs-council-approval']).toLowerCase() === 'true' || + String(metadata['needs-tsc-approval']).toLowerCase() === 'true' || + String(metadata.needs_council_approval).toLowerCase() === 'true' || + metadata.type?.toLowerCase() === 'standards track'; + + const row = document.createElement('tr'); + row.dataset.type = (metadata.type || 'core').toLowerCase(); + row.dataset.status = 'draft'; + row.dataset.councilApproval = needsApproval.toString(); + row.dataset.category = metadata.category || ''; + + const authors = metadata.author.split(',').map(author => { + const match = author.trim().match(/([^<(]+)(?:[<(]([^>)]+)[>)])?/); + if (!match) return author.trim(); + + const name = match[1].trim(); + const linkInfo = match[2]?.trim(); + + if (linkInfo) { + if (linkInfo.startsWith('@')) { + const username = linkInfo.substring(1); + return `${name}`; + } else if (linkInfo.includes('@')) { + return `${name}`; + } + } + return name; + }); + + row.innerHTML = ` + PR-${pr.number} + ${metadata.title} + ${authors.join(', ')} + ${needsApproval ? 'Yes' : 'No'} + `; + + tbody.appendChild(row); + }); + + table.querySelectorAll('th').forEach(header => { + header.addEventListener('click', function () { + const tbody = table.querySelector('tbody'); + const index = Array.from(header.parentNode.children).indexOf(header); + const isAscending = header.classList.contains('asc'); + const isNumeric = header.classList.contains('numeric'); + const isVersion = header.classList.contains('version'); + + Array.from(tbody.querySelectorAll('tr')) + .sort((rowA, rowB) => { + let cellA = rowA.querySelectorAll('td')[index].textContent; + let cellB = rowB.querySelectorAll('td')[index].textContent; + + if (isNumeric && cellA.startsWith('PR-') && cellB.startsWith('PR-')) { + // Extract numbers from PR-XXX format + const numA = parseInt(cellA.replace('PR-', '')); + const numB = parseInt(cellB.replace('PR-', '')); + return (numA - numB) * (isAscending ? 1 : -1); + } + + // Version sorting logic + if (isVersion) { + cellA = cellA.replace('v', '').split('.').map(Number); + cellB = cellB.replace('v', '').split('.').map(Number); + return cellA > cellB ? (isAscending ? 1 : -1) : cellA < cellB ? (isAscending ? -1 : 1) : 0; + } + + // Default sorting logic + return isNumeric ? (parseFloat(cellA) - parseFloat(cellB)) * (isAscending ? 1 : -1) : cellA.localeCompare(cellB) * (isAscending ? 1 : -1); + }) + .forEach(tr => tbody.appendChild(tr)); + + header.classList.toggle('asc', !isAscending); + header.classList.toggle('desc', isAscending); + + Array.from(header.parentNode.children) + .filter(th => th !== header) + .forEach(th => th.classList.remove('asc', 'desc')); + }); + }); + } +} + +document.addEventListener('DOMContentLoaded', () => { + new HIPPRIntegration(); +}); \ No newline at end of file From c2ce77db4551f6fc5f7b6ef05fef0c91bcd2cf34 Mon Sep 17 00:00:00 2001 From: Michael Heinrichs Date: Mon, 6 Jan 2025 19:29:35 +0100 Subject: [PATCH 08/12] HIP-991 update according to latest discussion (#1079) Signed-off-by: Michael Heinrichs Signed-off-by: mgarbs --- HIP/hip-991.md | 246 ++++++++++--------------------------------------- 1 file changed, 50 insertions(+), 196 deletions(-) diff --git a/HIP/hip-991.md b/HIP/hip-991.md index 01ed0fd15..89348a668 100644 --- a/HIP/hip-991.md +++ b/HIP/hip-991.md @@ -2,6 +2,7 @@ hip: 991 title: Permissionless revenue-generating Topic Ids for Topic Operators author: Michael Kantor (@kantorcodes), Ty Smith (@ty-swirldslabs) +working-group: Michael Heinrichs (@netopyr) requested-by: TierBot type: Standards Track category: Core @@ -10,7 +11,7 @@ status: Accepted last-call-date-time: 2024-07-24T07:00:00Z created: 2024-06-14 discussions-to: https://github.com/hashgraph/hedera-improvement-proposal/pull/991 -updated: 2024-09-11 +updated: 2024-12-05 --- ## Abstract @@ -90,65 +91,48 @@ We propose adding a fixed fee mechanism to the Hedera Consensus Service (HCS) fo * The Fee Schedule Key can be updated according to the same rules that currently apply to the Submit key. In addition, to update the Fee Schedule Key, the new key must sign the transaction. * If the topic was created without a Fee Schedule Key, the key cannot be added later. -#### Fee Payment Pre-Requisites - -* The fee payer must set an allowance (total fees and maximum fee per message) in HBAR or HTS tokens to be used to pay fees for message submissions on an HCS topic basis. -* Allowance granted to a Topic ID does not allow the Topic's Admin Key (or any of the Topic's keys) to spend that allowance outside of fee payments. - -#### Fee Payment Pre-Requisites (SDK) - -* When using the official SDK, the allowance transaction uses the `MAX_FEES` value for total fees and maximum fees per message by default. Developers must explicitly set these values if they don't want the defaults to be applied. -* When sending HCS messages via the SDKs, applications must set the `allowCustomFeesPayment` flag to `true`. -* The default value of the `allowCustomFeesPayment` flag is false. -* If the `allowCustomFeesPayment` flag is not set or is not true, the SDK returns an error. -* The `allowCustomFeesPayment` flag exists only in the SDK and isn't enforced by the network. - -#### Allowance Transactions - -* An account submits a transaction to grant an allowance (total fees and maximum fee per message) to a topic for both HBAR and HTS tokens. This transaction type reuses the existing allowance concept, with no need for a separate delete allowance transaction. To remove an allowance, the user will submit the same transaction with the allowance values set to 0 or a dedicated flag indicating allowance removal. -* This allowance only permits the topic to pay for the custom fees when messages are submitted, not for any other purpose. - #### Fee Payment -* The account submitting a message to the topic will cover network transaction fees, and if necessary, a custom fee via the approved allowance. The topic initiates the transfer of the custom fee to the fee collector using `transferFrom`, moving funds from the message sender's account to the designated fee collector. -* No balance will be held by the topic itself. Funds remain in the sender's account, and insufficient funds will result in the message submission failing with an appropriate error. The sender still pays network fees for failed transactions. +* A `ConsensusSubmitMessageTransactionBody` will include a new optional field `max_custom_fee` that a user can set to limit the paid custom fees. +* The account submitting a message to the topic will cover network transaction fees, and if necessary, a custom fee. The topic initiates the transfer of the custom fee to the fee collector using a synthetic `CryptoTransfer`, moving funds from the message sender's account to the designated fee collector. +* If the fee of submitting a message exceeds the `max_custom_fee`, the transaction will fail with an appropriate error. The sender still pays node and network fees for failed transactions. +* No balance will be held by the topic itself. Funds remain in the sender's account, and insufficient funds will result in the message submission failing with an appropriate error. The sender still pays node and network fees for failed transactions. #### Fee Exclusions -* Topics can have a Fee Exempt Key List (FEKL) -* FEKL can be set at creation time -* FEKL can be updated with a `ConsensusUpdateTopicTransaction` signed by the topic's Admin Key -* FEKL has a maximum of `MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST` entries. -* If a TopicMessageSubmitTransaction is submitted to the network and it contains a signature from a key included in the FEKL, no fees will be charged for that message. +* Topics can have a Fee Exempt Key List +* The Fee Exempt Key List can be set at creation time +* The Fee Exempt Key List can be updated with a `ConsensusUpdateTopicTransaction` signed by the topic's Admin Key +* The Fee Exempt Key List has a maximum of `MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST` entries. +* If a TopicMessageSubmitTransaction is submitted to the network and it contains a signature from a key included in the Fee Exempt Key List, no fees will be charged for that message. ##### Handling Duplicates -If the FEKL list contains duplicate keys, the transaction will fail with an `FEKL_CONTAINS_DUPLICATED_KEYS` error. This ensures that duplicate entries are not silently ignored, preventing potential bugs or issues in the calling code. +If the Fee Exempt Key List contains duplicate keys, the `ConsensusCreateTopicTransaction`, respectively `ConsensusUpdateTopicTransaction`, will fail with a `FEE_EXEMPT_KEY_LIST_CONTAINS_DUPLICATED_KEYS` error. This ensures that duplicate entries are not silently ignored, preventing potential bugs or issues in the calling code. ##### Signatures for Invalid/Inactive/Deleted Accounts -The FEKL list will only require keys to be formally valid as per protobuf specifications, meaning they must be correctly formatted key structures. These are keys, not accounts. Even if an account associated with a key is inactive, deleted, or non-existent, the key itself can still be added to the FEKL. +The Fee Exempt Key List will only require keys to be formally valid as per protobuf specifications, meaning they must be correctly formatted key structures. These are keys, not accounts. Even if an account associated with a key is inactive, deleted, or non-existent, the key itself can still be added to the Fee Exempt Key List. ##### Threshold Keys -Threshold keys are supported in the FEKL. To avoid paying custom fees, the threshold must be met, meaning that the number of required signatures from the FEKL must be satisfied for a message submission. Any valid threshold key configuration will be processed normally. +Threshold keys are supported in the Fee Exempt Key List. To avoid paying custom fees, the threshold must be met, meaning that the number of required signatures from the Fee Exempt Key List must be satisfied for a message submission. Any valid threshold key configuration will be processed normally. -##### Pre-Filled Keys in FEKL +##### Pre-Filled Keys in Fee Exempt Key List -When creating a topic, the FEKL list is independent and must be explicitly populated. By default, the FEKL will not include the Admin Key, Submit Key, or Fee Schedule Key, unless explicitly provided in the creation or update transaction. The SDK might suggest default behavior, but the HIP doesn't enforce automatic inclusion of these keys in the FEKL. +When creating a topic, the Fee Exempt Key List is independent and must be explicitly populated. By default, the Fee Exempt Key List will not include the Admin Key, Submit Key, or Fee Schedule Key, unless explicitly provided in the creation or update transaction. The SDK might suggest default behavior, but the HIP doesn't enforce automatic inclusion of these keys in the Fee Exempt Key List. ### HIP Parameters -After discussing with the engineering team, the HIP parameters are defined as follow: +The HIP parameters are defined as follow: * `MAX_CUSTOM_FEE_ENTRIES_FOR_TOPICS = 10` * `MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST = 10` -* `MAX_FEE = unsigned int 64` ### User Flows and Interaction * Users will specify the fee settings during the topic creation process through a simple interface in their Hedera client (refer to the creation of token custom fees/fixed fee for reference). -* Before submitting a message to a topic through an application or wallet interface, users must set an allowance and a maximum fee per message. +* When submitting a message to a topic with custom fees through an application or wallet interface, users must set the maximum fee for the message. Alternatively, users can add a flag to accept all custom fees from a topic id. If the topic has no custom fees, neither is required. * In case of user wallets, applications show the custom fees to the user before submitting the message. * Operators will get fee collections and distributions automatically through the custom fees just like they do in the token service currently. @@ -193,7 +177,7 @@ message ConsensusCreateTopicTransactionBody { repeated Key fee_exempt_key_list = 9; /** - * The custom fee to be assessed during a message submission to this topic + * The custom fee to be assessed during a message submission to this topic. Empty if no custom fees are applied. */ repeated ConsensusCustomFee custom_fees = 10; } @@ -201,23 +185,23 @@ message ConsensusCreateTopicTransactionBody { #### ConsensusUpdateTopicTransactionBody -The `ConsensusUpdateTopicTransactionBody` message is updated to include the optional Fee Schedule Key, the optional Fee Exempt Key List, and the `custom_fees` property for specifying fixed fees during topic creation. +The `ConsensusUpdateTopicTransactionBody` message is updated to include the optional Fee Schedule Key, the optional Fee Exempt Key List, and the `custom_fees` property for specifying fixed fees during topic update. ```protobuf message ConsensusUpdateTopicTransactionBody { [..] /** - * Access control for update/delete of custom fees. Null if there is no key. + * Access control for update/delete of custom fees. Null if the key should not be updated. */ Key fee_schedule_key = 10; /** - * If the transaction contains a signer from this list, no custom fees are applied. + * If the transaction contains a signer from this list, no custom fees are applied. Null if the list should not be updated. */ FeeExemptKeyList fee_exempt_key_list = 11; /* - * The custom fee to be assessed during a message submission to this topic + * The custom fee to be assessed during a message submission to this topic. Null if the fees should not be updated. */ ConsensusCustomFeeList custom_fees = 12; @@ -232,6 +216,25 @@ message ConsensusCustomFeeList { } ``` +#### ConsensusSubmitMessageTransactionBody + +The `ConsensusSubmitMessageTransactionBody` message is updated to include the optional `max_custom_fees` property for specifying the maximum fee that the user is willing to pay for the message. + +```protobuf +message ConsensusSubmitMessageTransactionBody { + [..] + /** + * The maximum custom fee that the user is willing to pay for the message. This field will be ignored if `accept_all_custom_fees` is set to `true`. + */ + repeated FixedFee max_custom_fees = 4; + + /** + * If set to true, the transaction will accept all custom fees from the topic id + */ + bool accept_all_custom_fees = 5; +} +``` + #### ConsensusTopicInfo The `ConsensusTopicInfo` message is updated to include the Fee Schedule Key and the current list of custom fixed fees associated with the topic. @@ -256,155 +259,15 @@ message ConsensusTopicInfo { } ``` -#### ConsensusApproveAllowanceTransactionBody - -The `ConsensusApproveAllowanceTransactionBody` message is added and includes one or more `ConsensusCryptoFeeScheduleAllowance` and one or more `ConsensusTokenFeeScheduleAllowance` messages. - -```protobuf -message ConsensusApproveAllowanceTransactionBody { - /** - * List of hbar allowances approved by the account owner. - */ - repeated ConsensusCryptoFeeScheduleAllowance consensus_crypto_fee_schedule_allowances = 4; - - /** - * List of fungible token allowances approved by the account owner. - */ - repeated ConsensusTokenFeeScheduleAllowance consensus_token_fee_schedule_allowances = 5; -} -``` - -#### ConsensusCryptoFeeScheduleAllowance - -This is a new protobuf message definition to enable crypto allowance for topics. - -```protobuf -/** - * An approved allowance of hbar transfers for a spender. - */ -message ConsensusCryptoFeeScheduleAllowance { - /** - * The account ID of the hbar owner (ie. the grantor of the allowance). - */ - AccountID owner = 1; - - /** - * The topic ID enabled to spend fees from the hbar allowance. - */ - TopicID spender = 2; - - /** - * The amount of the spender's allowance in tinybars. - */ - uint64 amount = 3; - - /** - * The maximum amount of the spender's token allowance per message. - */ - uint64 amount_per_message = 4; -} -``` - -#### ConsensusTokenFeeScheduleAllowance - -This is a new protobuf message definition to enable token allowance for topics. - -```protobuf -/** - * An approved allowance of fungible token transfers for a spender. - */ -message ConsensusTokenFeeScheduleAllowance { - /** - * The token that the allowance pertains to. - */ - TokenID tokenId = 1; - - /** - * The account ID of the token owner (ie. the grantor of the allowance). - */ - AccountID owner = 2; - - /** - * The topic ID enabled to spend fees from the token allowance. - */ - TopicID spender = 3; - - /** - * The maximum amount of the spender's token allowance. - */ - uint64 amount = 4; - - /** - * The maximum amount of the spender's token allowance per message. - */ - uint64 amount_per_message = 5; -} -``` - ### Mirror Node To comply with this HIP, the Mirror Node provides the following features: -* Support for querying a topic's allowances and custom fees via REST APIs. This may include listing all accounts that have granted allowances to a particular topic. +* Support for querying a topic's custom fees via REST APIs. * Custom fees should be able to be queried by topic, in the same structure as custom fees exist and can be queried on token entities. #### REST API changes -Here is the list of endpoints affected by this HIP and the expected changes. - -* `/api/v1/accounts/{idOrAliasOrEvmAddress}/allowances/crypto` - * The endpoint lists allowances set by both `CONSENSUSAPPROVEALLOWANCE` and `CRYPTOAPPROVEALLOWANCE` - * Changes to query parameters. Topic IDs are valid parameters for the `spender.id` field. - * Changes in the body of the response. The response includes the new `amount_per_message` field, and may include topic IDs in the spender field. A sample response payload follows. - - ```json - { - "allowances": [ - { - "amount": 75, - "amount_per_message": 5, - "amount_granted": 100, - "owner": "0.0.2", - "spender": "0.0.2", - "timestamp": { - "from": "1586567700.453054000", - "to": "1586567700.453054000" - } - } - ], - "links": { - "next": null - } - } - ``` - -* `/api/v1/accounts/{idOrAliasOrEvmAddress}/allowances/tokens` - * The endpoint lists allowances set by both `CONSENSUSAPPROVEALLOWANCE` and `CRYPTOAPPROVEALLOWANCE` - * Changes to query parameters. Topic IDs are valid parameters for the `spender.id` field. - * Changes in the body of the response. The response includes the new `amount_per_message` field, and may include topic IDs in the spender field. A sample response payload follows. - - ```json - { - "allowances": [ - { - "amount": 75, - "amount_per_message": 5, - "amount_granted": 100, - "owner": "0.0.2", - "spender": "0.0.2", - "timestamp": { - "from": "1586567700.453054000", - "to": "1586567700.453054000" - }, - "token_id": "0.0.2" - } - ], - "links": { - "next": null - } - } - ``` - * `/api/v1/topics/{topicId}` * Changes to the body of the response. The response should include three new fields: `fee_schedule_key`, `fee_exempt_key_list`, and `custom_fee`. The fields will expose any data defined in their corresponding protobuffer definition. A sample response payload follows. @@ -455,10 +318,6 @@ Here is the list of endpoints affected by this HIP and the expected changes. } ``` -* `/api/v1/transactions` - * Changes in query parameters. The new allowance transaction type `CONSENSUSAPPROVEALLOWANCE` is included in the list of supported transaction type for the `transactiontype` parameter. - * Changes in the body of the response. The endpoint returns the same fields as `CRYPTOAPPROVEALLOWANCE`. - ### SDKs This document does not include the details of the implementation updates required by the SDKs to comply with the HIP-991 specifications. @@ -472,9 +331,7 @@ There are no known backward compatibility issues. Existing topics without custom ## Security Implications -The introduction of custom fees adds another layer of economic control, but also introduces potential vectors for abuse, such as fee manipulation. To address these issues, this HIP adheres to current security requirements regarding the authorization of moving user funds. In particular, before being able to send paid HCS messages to a topic, users should first set an allowance for the recipient topic, just as in the case of HTS, to allow payment of fixed or custom fees. The user can also set a maximum charge per message. - -At the network level, the default value for both allowance parameters is 0. At SDK level, the fees (total maximum amount and maximum fee per message) are always the maximum amount `MAX_FEE`. For this reason, the SDK documentation should contain a considerable callout on this point and be clear about the implications of not changing these default values. +The introduction of custom fees adds another layer of economic control, but also introduces potential vectors for abuse, such as fee manipulation. To address these issues, this HIP adheres to current security requirements regarding the authorization of moving user funds. In particular, the user can set a maximum charge for each message. ## How to Teach This @@ -496,7 +353,7 @@ const feeCollectorAccountId = AccountId.fromString("0.0.12345"); // Define the fixed fee const topicCustomFee = new TopicCustomFee() - .setAmount(100) // 100 tokens are transferred to the fee collecting account each time this token is transferred + .setAmount(100) // 100 tokens are transferred to the fee collecting account each time a message is submitted to the topic .setDenominatingTokenId(TokenId.fromString("0.0.56789")) // // The token to charge the fee in. HBAR if unset .setFeeCollectorAccountId(feeCollectorAccountId); // 100 tokens are sent to this account for each HCS message to the topic @@ -512,16 +369,14 @@ console.log(`Created topic with ID: ${receipt.topicId}`); [...] -// Set allowance -const spenderTopicId = new TopicID("0.0.54321") -transactionResponse = new TopicAllowanceApproveTransaction() - .approveHbarAllowance(ownerAccount, spenderTopicId, Hbar.from(100), Hbar.from(2)); // Set owner, topicId, maximum allowance, and max per message - -[...] +// Define max custom fee +const maxCustomFee = new FixedFee() + .setAmount(100) // a maximum of 100 tokens is paid for submitting the message + .setDenominatingTokenId(TokenId.fromString("0.0.56789")); // The token to charge the fee in. HBAR if unset // Send message to the topic transactionResponse = await new TopicMessageSubmitTransaction({ topicId: topicId, message: "Hello, HCS!" }) - .setAllowCustomFeesPayment(true) // Make the agreement on payment explicit at the application level. Useful for any backend / non-front-user-facing application. + .setMaxCustomFees([maxCustomFee]) // Set the maximum fee for the message .execute(client); ``` @@ -531,7 +386,6 @@ This implementation shows the creation of a topic where each message submission ## Rejected Ideas * Setting a mandatory network enforced `allowCustomFeesPayment` flag in HCS message transactions to allow payment of fees. -* Setting 0 as default in SDKs for maximum fees. ## References From e4576853b9b2b9589e2979f5d645d1afae711d7a Mon Sep 17 00:00:00 2001 From: mgarbs Date: Tue, 7 Jan 2025 09:21:02 -0500 Subject: [PATCH 09/12] Remove line setting github token Signed-off-by: mgarbs --- _includes/head.html | 1 - 1 file changed, 1 deletion(-) diff --git a/_includes/head.html b/_includes/head.html index 2efbe7cb2..4e6cffa83 100644 --- a/_includes/head.html +++ b/_includes/head.html @@ -1,5 +1,4 @@ - From 3657ac6fec3f5d1060dd2748780bacdaafc8d083 Mon Sep 17 00:00:00 2001 From: mgarbs Date: Tue, 7 Jan 2025 09:21:44 -0500 Subject: [PATCH 10/12] Add workflow to fetch hip data from prs Signed-off-by: mgarbs --- .github/workflows/update-draft-hips.yml | 118 ++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .github/workflows/update-draft-hips.yml diff --git a/.github/workflows/update-draft-hips.yml b/.github/workflows/update-draft-hips.yml new file mode 100644 index 000000000..c1d99bc26 --- /dev/null +++ b/.github/workflows/update-draft-hips.yml @@ -0,0 +1,118 @@ +name: Update Draft HIPs Data + +on: + schedule: + - cron: "0 */6 * * *" # Runs every 6 hours + workflow_dispatch: # Allows manual triggering + +jobs: + update-draft-hips: + if: github.ref == 'refs/heads/main' # Only run on main branch + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Create Script + run: | + mkdir -p _data + cat << 'EOF' > fetch-draft-hips.js + const https = require('https'); + + async function makeGraphQLRequest(query, token) { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: '/graphql', + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'Node.js' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', () => resolve(JSON.parse(data))); + }); + + req.on('error', reject); + req.write(JSON.stringify({ query })); + req.end(); + }); + } + + async function getAllPRs() { + const query = ` + query { + repository(name: "hedera-improvement-proposal", owner: "hashgraph") { + pullRequests(first: 100, states: [OPEN], orderBy: {field: CREATED_AT, direction: DESC}) { + nodes { + title + number + url + headRefOid + files(first: 100) { + edges { + node { + path + additions + deletions + } + } + } + author { + login + } + } + } + } + } + `; + + try { + const result = await makeGraphQLRequest(query, process.env.GITHUB_TOKEN); + + if (result.errors) { + console.error('GraphQL errors:', result.errors); + process.exit(1); + } + + return result.data.repository.pullRequests.nodes; + } catch (error) { + console.error('Error fetching PRs:', error); + throw error; + } + } + + // Run the main function + getAllPRs().then(prs => { + const fs = require('fs'); + fs.writeFileSync('_data/draft_hips.json', JSON.stringify(prs, null, 2)); + }).catch(error => { + console.error('Failed to fetch PRs:', error); + process.exit(1); + }); + EOF + + - name: Run Script + run: node fetch-draft-hips.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Commit and Push Changes + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add _data/draft_hips.json + git commit -m "Update draft HIPs data [skip ci]" || echo "No changes to commit" + git push origin main || echo "No changes to push" From 7b29d1ce3285e771a2ad7155848fd6a8fb5d7860 Mon Sep 17 00:00:00 2001 From: mgarbs Date: Tue, 7 Jan 2025 09:22:47 -0500 Subject: [PATCH 11/12] Add query result file to be included in build Signed-off-by: mgarbs --- _config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/_config.yml b/_config.yml index 770718471..a15d7d38d 100644 --- a/_config.yml +++ b/_config.yml @@ -32,6 +32,9 @@ defaults: values: layout: "hip" +include: + - _data + # Exclude from processing. # The following items will not be processed, by default. Create a custom list # to override the default setting. From c23f64e4819880fba057c7d923d3d80798ef939e Mon Sep 17 00:00:00 2001 From: mgarbs Date: Tue, 7 Jan 2025 09:23:32 -0500 Subject: [PATCH 12/12] Update script to fetch workflow json result Signed-off-by: mgarbs --- assets/js/pr-integration.js | 69 +++++++++---------------------------- 1 file changed, 16 insertions(+), 53 deletions(-) diff --git a/assets/js/pr-integration.js b/assets/js/pr-integration.js index 06e171141..121f97b6d 100644 --- a/assets/js/pr-integration.js +++ b/assets/js/pr-integration.js @@ -69,51 +69,14 @@ class HIPPRIntegration { async fetchPRData() { try { - const token = document.querySelector('meta[name="github-token"]').content; - - const query = { - query: `query { - repository(name: "hedera-improvement-proposal", owner: "hashgraph") { - pullRequests(first: 100, orderBy: {field: CREATED_AT, direction: DESC}, states: [OPEN]) { - nodes { - title - number - url - headRefOid - files(last: 100) { - edges { - node { - path - additions - deletions - } - } - } - author { - login - } - } - } - } - }` - }; - - const response = await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(query) - }); + // Try with site.baseurl if it's set in _config.yml + const baseUrl = document.querySelector('meta[name="site-baseurl"]')?.content || ''; + const response = await fetch(`${baseUrl}/_data/draft_hips.json`); - const data = await response.json(); - - if (!response.ok || data.errors) { - console.error('GraphQL errors:', data.errors); - return null; + if (!response.ok) { + throw new Error('Failed to fetch draft HIPs data'); } - return data.data.repository.pullRequests.nodes; + return await response.json(); } catch (error) { console.error('Error in fetchPRData:', error); throw error; @@ -123,14 +86,14 @@ class HIPPRIntegration { async filterHIPPRs(prs) { const validHips = []; const seenPRs = new Set(); - + for (const pr of prs) { if (seenPRs.has(pr.number)) continue; - + const mdFiles = pr.files.edges.filter(file => file.node.path.endsWith('.md')); let bestMetadata = null; let bestFile = null; - + // Try all MD files and pick the one with the most complete metadata for (const file of mdFiles) { try { @@ -138,16 +101,16 @@ class HIPPRIntegration { const response = await fetch(contentUrl); const content = await response.text(); const metadata = this.parseHIPMetadata(content); - + // Skip template files and empty metadata if (file.node.path.includes('template') || !metadata.title) { continue; } - + // If we don't have metadata yet, or this file has a better title - if (!bestMetadata || - (metadata.title && metadata.title.length > 3 && - (!bestMetadata.title || metadata.title.length > bestMetadata.title.length))) { + if (!bestMetadata || + (metadata.title && metadata.title.length > 3 && + (!bestMetadata.title || metadata.title.length > bestMetadata.title.length))) { bestMetadata = metadata; bestFile = file; } @@ -155,7 +118,7 @@ class HIPPRIntegration { console.error(`Error checking file ${file.node.path}:`, error); } } - + // If we found valid metadata, add it to the results if (bestMetadata && bestFile) { validHips.push({ @@ -166,7 +129,7 @@ class HIPPRIntegration { seenPRs.add(pr.number); } } - + return validHips; }