Repository navigation
feat: add signer-based signing with sign_with, sign_with_operator and set_operator_with #2682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
danielmarv
merged 9 commits into
hiero-ledger:main
from
danielmarv:feat/signer-based-signing
Oct 10, 2026
+1,030
−51
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
5d907b3
feat: add signer-based signing with sign_with, sign_with_operator and…
danielmarv 99728aa
Merge branch 'main' into feat/signer-based-signing
danielmarv e4d4a8d
Merge branch 'main' into feat/signer-based-signing
danielmarv 392e797
Merge branch 'main' into feat/signer-based-signing
danielmarv 6ff0ee5
chore(examples): drop redundant load_dotenv from external signer example
danielmarv e603f4b
Merge branch 'main' into feat/signer-based-signing
danielmarv 3955cfa
Merge branch 'main' into feat/signer-based-signing
danielmarv 6b9cb2b
Merge branch 'main' into feat/signer-based-signing
danielmarv 052a5de
Merge branch 'main' into feat/signer-based-signing
danielmarv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| """ | ||
| Set up a client whose operator signs through a callback instead of a private key. | ||
|
|
||
| `Client.set_operator_with(account_id, public_key, signer)` lets the operator key | ||
| stay in an HSM or a cloud KMS. The client then uses the signer to pay for every | ||
| transaction and query it executes. | ||
|
|
||
| This example simulates the HSM with a callback that wraps OPERATOR_KEY. | ||
|
|
||
| Usage: | ||
| uv run examples/client/client_set_operator_with.py | ||
| python examples/client/client_set_operator_with.py | ||
| """ | ||
|
|
||
| import os | ||
| import sys | ||
|
|
||
| from dotenv import load_dotenv | ||
|
|
||
| from hiero_sdk_python import ( | ||
| AccountCreateTransaction, | ||
| AccountId, | ||
| AccountInfoQuery, | ||
| Client, | ||
| Hbar, | ||
| Network, | ||
| PrivateKey, | ||
| ResponseCode, | ||
| Signer, | ||
| ) | ||
|
|
||
|
|
||
| load_dotenv() | ||
|
|
||
| NETWORK_NAME = os.getenv("NETWORK", "testnet").lower() | ||
| OPERATOR_ID = os.getenv("OPERATOR_ID") | ||
| OPERATOR_KEY = os.getenv("OPERATOR_KEY") | ||
|
|
||
|
|
||
| def simulated_hsm(private_key: PrivateKey) -> Signer: | ||
| """ | ||
| Return a signer callback standing in for an HSM or KMS. | ||
|
|
||
| A real signer would send body_bytes to the HSM and return the raw signature. | ||
| """ | ||
|
|
||
| def signer(body_bytes: bytes) -> bytes: | ||
| print(f" HSM asked to sign {len(body_bytes)} bytes") | ||
| return private_key.sign(body_bytes) | ||
|
|
||
| return signer | ||
|
|
||
|
|
||
| def setup_client() -> Client: | ||
| """Create a client whose operator signs through the simulated HSM.""" | ||
| if not OPERATOR_ID or not OPERATOR_KEY: | ||
| raise ValueError("OPERATOR_ID and OPERATOR_KEY environment variables are required") | ||
|
|
||
| operator_id = AccountId.from_string(OPERATOR_ID) | ||
| operator_key = PrivateKey.from_string(OPERATOR_KEY) | ||
|
|
||
| # Only the public key and the callback reach the client | ||
| client = Client(Network(NETWORK_NAME)).set_operator_with( | ||
| operator_id, operator_key.public_key(), simulated_hsm(operator_key) | ||
| ) | ||
|
|
||
| print(f"Network: {client.network.network}") | ||
| print(f"Client initialized with signer-based operator {client.operator_account_id}") | ||
| print(f"Operator private key held by the client: {client.operator_private_key}") | ||
| return client | ||
|
|
||
|
|
||
| def create_account(client: Client) -> AccountId: | ||
| """Create an account; the operator pays and signs through the HSM.""" | ||
| print("Creating an account...") | ||
| receipt = ( | ||
| AccountCreateTransaction() | ||
| .set_key_without_alias(PrivateKey.generate_ed25519().public_key()) | ||
| .set_initial_balance(Hbar(1)) | ||
| .execute(client) | ||
| ) | ||
| if receipt.status != ResponseCode.SUCCESS: | ||
| raise RuntimeError(f"Account creation failed with status: {ResponseCode(receipt.status).name}") | ||
|
|
||
| print(f"Created account {receipt.account_id}") | ||
| return receipt.account_id | ||
|
|
||
|
|
||
| def query_account_info(client: Client, account_id: AccountId) -> None: | ||
| """Run a paid query; the query payment is signed through the HSM too.""" | ||
| print("Querying account info...") | ||
| info = AccountInfoQuery().set_account_id(account_id).execute(client) | ||
| print(f"Account {info.account_id} has a balance of {info.balance}") | ||
|
|
||
|
|
||
| def main(): | ||
| """ | ||
| 1. Set up a client with set_operator_with() and a simulated HSM signer. | ||
|
|
||
| 2. Execute a transaction paid for by the signer-based operator. | ||
| 3. Execute a paid query, whose payment is signed by the same signer. | ||
| """ | ||
| try: | ||
| client = setup_client() | ||
|
|
||
| account_id = create_account(client) | ||
| query_account_info(client, account_id) | ||
|
|
||
| client.close() | ||
|
|
||
| except Exception as exc: | ||
| print(f"Error: {exc}") | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
105 changes: 105 additions & 0 deletions
105
examples/transaction/transaction_sign_with_external_signer.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| """ | ||
| Sign a transaction with an external signer instead of a private key. | ||
|
|
||
| `sign_with(public_key, signer)` hands each transaction body to a callback and | ||
| attaches the signature it returns. This is how keys held in an HSM or a cloud KMS | ||
| sign transactions: the private key never enters the SDK. | ||
|
|
||
| This example simulates the HSM with a callback that wraps a locally generated key. | ||
|
|
||
| Usage: | ||
| uv run examples/transaction/transaction_sign_with_external_signer.py | ||
| python examples/transaction/transaction_sign_with_external_signer.py | ||
| """ | ||
|
|
||
| import sys | ||
|
|
||
| from hiero_sdk_python import ( | ||
| AccountCreateTransaction, | ||
| AccountId, | ||
| Client, | ||
| Hbar, | ||
| PrivateKey, | ||
| ResponseCode, | ||
| Signer, | ||
| TransferTransaction, | ||
| ) | ||
|
|
||
|
|
||
| def setup_client() -> Client: | ||
| """Initialize the client from the OPERATOR_ID and OPERATOR_KEY environment variables.""" | ||
| client = Client.from_env() | ||
| print(f"Network: {client.network.network}") | ||
| print(f"Client initialized with operator {client.operator_account_id}") | ||
| return client | ||
|
|
||
|
|
||
| def simulated_hsm(private_key: PrivateKey) -> Signer: | ||
| """ | ||
| Return a signer callback standing in for an HSM or KMS. | ||
|
|
||
| A real signer would send body_bytes to the HSM and return the raw signature: | ||
| 64 bytes for Ed25519, or the 64-byte r || s over keccak256(body_bytes) for ECDSA. | ||
| """ | ||
|
|
||
| def signer(body_bytes: bytes) -> bytes: | ||
| print(f" HSM asked to sign {len(body_bytes)} bytes") | ||
| return private_key.sign(body_bytes) | ||
|
|
||
| return signer | ||
|
|
||
|
|
||
| def create_account(client: Client, key: PrivateKey) -> AccountId: | ||
| """Create an account controlled by the HSM key.""" | ||
| receipt = ( | ||
| AccountCreateTransaction().set_key_without_alias(key.public_key()).set_initial_balance(Hbar(2)).execute(client) | ||
| ) | ||
| if receipt.status != ResponseCode.SUCCESS: | ||
| raise RuntimeError(f"Account creation failed with status: {ResponseCode(receipt.status).name}") | ||
|
|
||
| print(f"Created account {receipt.account_id} with an ECDSA key held by the simulated HSM") | ||
| return receipt.account_id | ||
|
|
||
|
|
||
| def transfer_with_external_signer(client: Client, account_id: AccountId, key: PrivateKey) -> None: | ||
| """Transfer HBAR out of the HSM account, signing through the callback.""" | ||
| tx = ( | ||
| TransferTransaction() | ||
| .add_hbar_transfer(account_id, -Hbar(1).to_tinybars()) | ||
| .add_hbar_transfer(client.operator_account_id, Hbar(1).to_tinybars()) | ||
| .freeze_with(client) | ||
| ) | ||
|
|
||
| # The signer is called once per transaction body, i.e. once per node | ||
| tx.sign_with(key.public_key(), simulated_hsm(key)) | ||
| print(f"Transaction signed by the HSM key: {tx.is_signed_by(key.public_key())}") | ||
|
|
||
| # The operator pays the fee, so execute() adds the operator signature | ||
| receipt = tx.execute(client) | ||
| if receipt.status != ResponseCode.SUCCESS: | ||
| raise RuntimeError(f"Transfer failed with status: {ResponseCode(receipt.status).name}") | ||
|
|
||
| print(f"Transfer succeeded: {receipt.status == ResponseCode.SUCCESS}") | ||
|
|
||
|
|
||
| def main(): | ||
| """ | ||
| 1. Set up a client with an operator. | ||
|
|
||
| 2. Create an account whose key lives in a (simulated) HSM. | ||
| 3. Transfer HBAR from that account, signing with sign_with() instead of sign(). | ||
| """ | ||
| try: | ||
| client = setup_client() | ||
| hsm_key = PrivateKey.generate_ecdsa() | ||
|
|
||
| account_id = create_account(client, hsm_key) | ||
| transfer_with_external_signer(client, account_id, hsm_key) | ||
|
|
||
| except Exception as exc: | ||
| print(f"Error: {exc}") | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.