Skip to content

Commit 19a0ad8

Browse files
authored
Merge branch 'main' into dependabot/github_actions/hiero-ledger/hiero-solo-action-0.19.0
2 parents edd68e2 + 2f646a8 commit 19a0ad8

20 files changed

Lines changed: 697 additions & 138 deletions

.github/scripts/coderabbit_plan_trigger.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,9 @@
22

33
const CODERABBIT_MARKER = '<!-- CodeRabbit Plan Trigger -->';
44

5-
async function triggerCodeRabbitPlan(github, owner, repo, issue, marker = CODERABBIT_MARKER, isDryRun = false) {
5+
async function triggerCodeRabbitPlan(github, owner, repo, issue, marker = CODERABBIT_MARKER) {
66
const comment = `${marker} @coderabbitai plan`;
77

8-
if (isDryRun) {
9-
console.log(`[DRY RUN] Would trigger CodeRabbit plan for issue #${issue.number}`);
10-
return true;
11-
}
12-
138
try {
149
await github.rest.issues.createComment({
1510
owner,
@@ -105,11 +100,8 @@ async function main({ github, context }) {
105100
return console.log(`CodeRabbit plan already triggered for #${issue.number}`);
106101
}
107102

108-
// Check for dry run (default to true if not specified, for safety)
109-
const isDryRun = (process.env.DRY_RUN || 'true').toLowerCase() === 'true';
110-
111103
// Post CodeRabbit plan trigger
112-
await triggerCodeRabbitPlan(github, owner, repo, issue, CODERABBIT_MARKER, isDryRun);
104+
await triggerCodeRabbitPlan(github, owner, repo, issue, CODERABBIT_MARKER);
113105

114106
logSummary(owner, repo, issue);
115107
} catch (err) {

.github/scripts/cron-admin-update-spam-list.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
const fs = require('fs').promises;
11+
const { LABEL_AUTOMATED } = require('./labels.js');
1112

1213
const SPAM_LIST_PATH = '.github/spam-list.txt';
1314
const dryRun = (process.env.DRY_RUN || 'false').toString().toLowerCase() === 'true';
@@ -229,7 +230,7 @@ module.exports = async ({github, context, core}) => {
229230
repo,
230231
title,
231232
body,
232-
labels: [LABEL_SPAM_LIST_UPDATE, 'automated']
233+
labels: [LABEL_SPAM_LIST_UPDATE, LABEL_AUTOMATED]
233234
});
234235
console.log('Issue created successfully');
235236
}

.github/scripts/labels.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Central location for label definitions to facilitate future changes.
3+
* This file serves as the single source of truth for all GitHub label names used by automated workflows.
4+
*/
5+
const LABEL_BROKEN_MARKDOWN_LINKS = 'notes: broken markdown links';
6+
const LABEL_AUTOMATED = 'notes: automated';
7+
8+
module.exports = {
9+
LABEL_BROKEN_MARKDOWN_LINKS,
10+
LABEL_AUTOMATED
11+
};

.github/workflows/bot-coderabbit-plan-trigger.yml

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,6 @@ name: CodeRabbit Plan Trigger
55
on:
66
issues:
77
types: [labeled]
8-
workflow_dispatch:
9-
inputs:
10-
dry_run:
11-
description: "Run without posting comments"
12-
required: false
13-
default: "true"
14-
type: choice
15-
options:
16-
- "true"
17-
- "false"
188

199
permissions:
2010
issues: write
@@ -28,10 +18,9 @@ jobs:
2818
cancel-in-progress: false
2919
# Only run when the newly added label is beginner, intermediate, or advanced (case-insensitive)
3020
if: >
31-
github.event_name == 'workflow_dispatch' ||
32-
(github.event.action == 'labeled' &&
21+
github.event.action == 'labeled' &&
3322
github.event.issue.state == 'open' &&
34-
contains(fromJson('["beginner","intermediate","advanced","Beginner","Intermediate","Advanced"]'), github.event.label.name))
23+
contains(fromJson('["beginner","intermediate","advanced","Beginner","Intermediate","Advanced"]'), github.event.label.name)
3524
3625
steps:
3726
- name: Harden the runner
@@ -45,7 +34,6 @@ jobs:
4534
- name: Trigger CodeRabbit Plan
4635
env:
4736
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48-
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }}
4937
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0
5038
with:
5139
script: |

.github/workflows/cron-pr-check-broken-links.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
name: Cron – Check Broken Markdown Links
22

3+
concurrency:
4+
group: cron-check-broken-markdown-links
5+
cancel-in-progress: false
6+
37
on:
48
schedule:
59
- cron: "0 0 1 * *"
@@ -42,12 +46,14 @@ jobs:
4246
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
4347
with:
4448
script: |
49+
const { LABEL_BROKEN_MARKDOWN_LINKS, LABEL_AUTOMATED } = require('./.github/scripts/labels.js');
50+
4551
// Determine if this is a dry run
4652
const isManual = context.eventName === 'workflow_dispatch';
47-
const dryRun = isManual ? String(context.payload.inputs.dry_run).toLowerCase() === 'true' : false;
53+
const dryRun = isManual ? String(context.payload?.inputs?.dry_run).toLowerCase() === 'true' : false;
4854
4955
// Labels configuration
50-
const targetLabels = ['broken-markdown-links', 'automated'];
56+
const targetLabels = [LABEL_BROKEN_MARKDOWN_LINKS, LABEL_AUTOMATED];
5157
const issueTitle = "Scheduled Markdown Link Check Found Broken Links";
5258
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
5359

docs/roadmap.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Hiero SDK Python — Roadmap
2+
3+
This is a high-level quarterly roadmap for the Hiero SDK Python project. It is a living document, updated during team discussions.
4+
5+
---
6+
7+
## 2026 Q1 (Underway)
8+
9+
- Implement [TCK](/tck) and [fuzzing tests](/tests/fuzz)
10+
- Implement [static workflow tests](/.github/workflows/)
11+
12+
## 2026 Q2
13+
14+
- Complete [TCK](/tck) tests
15+
- Implement 2 new HIPs
16+
17+
## 2026 Q3
18+
19+
- Improve core functionality robustness
20+
- Implement 2 new HIPs
21+
22+
## 2026 Q4
23+
24+
- Focus on remaining HIP implementations

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ requires-python = ">=3.10, <4"
1616
dependencies = [
1717
"protobuf>=4.21.12,<8",
1818
"grpcio>=1.76.0,<2",
19-
"cryptography>=44.0.1,<47",
19+
"cryptography>=44.0.1,<48",
2020
"requests>=2.31.0,<3",
2121
"pycryptodome>=3.18.0,<4",
2222
"eth-abi>=5.1.0,<6",

src/hiero_sdk_python/consensus/topic_create_transaction.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from hiero_sdk_python.account.account_id import AccountId
1313
from hiero_sdk_python.channels import _Channel
14+
from hiero_sdk_python.crypto.key import Key
1415
from hiero_sdk_python.Duration import Duration
1516
from hiero_sdk_python.executable import _Method
1617
from hiero_sdk_python.hapi.services import consensus_create_topic_pb2, transaction_pb2
@@ -19,7 +20,7 @@
1920
)
2021
from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee
2122
from hiero_sdk_python.transaction.transaction import Transaction
22-
from hiero_sdk_python.utils.key_utils import Key, key_to_proto
23+
from hiero_sdk_python.utils.key_utils import key_to_proto
2324

2425

2526
class TopicCreateTransaction(Transaction):

src/hiero_sdk_python/crypto/public_key.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,90 @@ def to_bytes_der(self) -> bytes:
413413
encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo
414414
)
415415

416+
@classmethod
417+
def _encode_vlq(cls, value: int) -> bytes:
418+
"""
419+
Encode a value in Variable-Length Quantity (VLQ) format.
420+
"""
421+
if value < 0:
422+
raise ValueError("VLQ value must be non-negative")
423+
if value == 0:
424+
return b"\x00"
425+
426+
encoded = bytearray()
427+
while value > 0:
428+
encoded.insert(0, value & 0x7F)
429+
value >>= 7
430+
431+
# Set the high bit (0x80) on all bytes except the last
432+
for i in range(len(encoded) - 1):
433+
encoded[i] |= 0x80
434+
435+
return bytes(encoded)
436+
437+
@classmethod
438+
def _encode_der_length(cls, length: int) -> bytes:
439+
"""Encode a DER length field per X.690 standards."""
440+
if length < 0:
441+
raise ValueError("DER length must be non-negative")
442+
if length < 0x80:
443+
return bytes([length])
444+
445+
length_bytes = length.to_bytes((length.bit_length() + 7) // 8, "big")
446+
return bytes([0x80 | len(length_bytes)]) + length_bytes
447+
448+
@classmethod
449+
def _encode_der_oid(cls, oid: str) -> bytes:
450+
"""
451+
Encode a dotted OID string into DER OID bytes including tag and length.
452+
"""
453+
parts = [int(part) for part in oid.split(".")]
454+
455+
is_valid = len(parts) >= 2 and parts[0] in (0, 1, 2) and parts[1] >= 0 and (parts[0] == 2 or parts[1] < 40)
456+
if not is_valid:
457+
raise ValueError(f"Invalid OID structure for '{oid}'")
458+
459+
first, second = parts[0], parts[1]
460+
461+
# Encode the combined root using VLQ to handle edge cases correctly
462+
encoded = bytearray(cls._encode_vlq(40 * first + second))
463+
464+
for value in parts[2:]:
465+
encoded.extend(cls._encode_vlq(value))
466+
467+
return bytes([0x06]) + cls._encode_der_length(len(encoded)) + bytes(encoded)
468+
469+
@classmethod
470+
def _encode_der_sequence(cls, content: bytes) -> bytes:
471+
"""Encode DER SEQUENCE tag, length, and content per X.690 standards."""
472+
return bytes([0x30]) + cls._encode_der_length(len(content)) + content
473+
474+
@classmethod
475+
def _encode_der_bit_string(cls, content: bytes) -> bytes:
476+
"""
477+
Encode DER BIT STRING tag, length, and content per X.690 standards.
478+
"""
479+
payload = bytes([0x00]) + content
480+
return bytes([0x03]) + cls._encode_der_length(len(payload)) + payload
481+
482+
def to_bytes_der_ecdsa_compressed(self) -> bytes:
483+
"""
484+
Returns DER-encoded SubjectPublicKeyInfo for secp256k1 using a compressed SEC1 point.
485+
Raises:
486+
ValueError: If this key is not ECDSA secp256k1.
487+
"""
488+
if not self.is_ecdsa():
489+
raise ValueError("Compressed ECDSA DER export is only supported for ECDSA keys")
490+
491+
# id-ecPublicKey + secp256k1 OID algorithm identifier
492+
algorithm_id = self._encode_der_sequence(
493+
self._encode_der_oid("1.2.840.10045.2.1") + self._encode_der_oid("1.3.132.0.10")
494+
)
495+
compressed_point = self.to_bytes_ecdsa(compressed=True)
496+
subject_public_key = self._encode_der_bit_string(compressed_point)
497+
498+
return self._encode_der_sequence(algorithm_id + subject_public_key)
499+
416500
#
417501
# ---------------------------------
418502
# Type-specific (Ed25519, ECDSA secp256k1) to hex string.
@@ -440,6 +524,12 @@ def to_string_der(self) -> str:
440524
"""
441525
return self.to_bytes_der().hex()
442526

527+
def to_string_der_ecdsa_compressed(self) -> str:
528+
"""
529+
Returns DER SPKI hex for ECDSA secp256k1 using a compressed SEC1 point.
530+
"""
531+
return self.to_bytes_der_ecdsa_compressed().hex()
532+
443533
def to_string_raw(self) -> str:
444534
"""
445535
Catch all ed25519 or ecdsa for convenience.

src/hiero_sdk_python/query/transaction_get_receipt_query.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,17 +277,22 @@ def _map_status_error(self, response: response_pb2.Response) -> PrecheckError |
277277
TransactionReceipt._from_proto(response.transactionGetReceipt.receipt, self.transaction_id),
278278
)
279279

280-
def _map_receipt_list(self, receipts: list[transaction_receipt_pb2.TransactionReceipt]) -> list[TransactionReceipt]:
280+
def _map_receipt_list(
281+
self, receipts: list[transaction_receipt_pb2.TransactionReceipt], include_parent_tx_id: bool = False
282+
) -> list[TransactionReceipt]:
281283
"""
282284
Maps a list of protobuf transaction receipts to TransactionReceipt objects.
283285
284286
Args:
285287
receipts: A list of protobuf TransactionReceipt objects
288+
include_parent_tx_id: If True, pass parent transaction_id to mapped receipts (for duplicates).
289+
If False, pass None (for child receipts).
286290
287291
Returns:
288292
A list of TransactionReceipt objects
289293
"""
290-
return [TransactionReceipt._from_proto(receipt_proto, self.transaction_id) for receipt_proto in receipts]
294+
transaction_id = self.transaction_id if include_parent_tx_id else None
295+
return [TransactionReceipt._from_proto(receipt_proto, transaction_id) for receipt_proto in receipts]
291296

292297
def execute(self, client: Client, timeout: int | float | None = None) -> TransactionReceipt:
293298
"""
@@ -315,12 +320,18 @@ def execute(self, client: Client, timeout: int | float | None = None) -> Transac
315320
parent = TransactionReceipt._from_proto(response.transactionGetReceipt.receipt, self.transaction_id)
316321

317322
if self.include_children:
318-
children = self._map_receipt_list(response.transactionGetReceipt.child_transaction_receipts)
323+
# Child receipts are sub-transactions; they don't need parent transaction_id
324+
children = self._map_receipt_list(
325+
response.transactionGetReceipt.child_transaction_receipts, include_parent_tx_id=False
326+
)
319327

320328
parent._set_children(children)
321329

322330
if self.include_duplicates:
323-
duplicates = self._map_receipt_list(response.transactionGetReceipt.duplicateTransactionReceipts)
331+
# Duplicate receipts are related to parent; keep parent transaction_id for context
332+
duplicates = self._map_receipt_list(
333+
response.transactionGetReceipt.duplicateTransactionReceipts, include_parent_tx_id=True
334+
)
324335

325336
parent._set_duplicates(duplicates)
326337

0 commit comments

Comments
 (0)