Signing#
Using different signing methods#
By default, an Account uses the signing method of OpenZeppelin’s account contract. If for any reason you want to use a different
signing algorithm, it is possible to create Account
with custom
Signer implementation.
from starknet_py.net.account.account import Account
from starknet_py.net.full_node_client import FullNodeClient
from starknet_py.net.models import StarknetChainId, Transaction
from starknet_py.net.signer import BaseSigner
from starknet_py.utils.typed_data import TypedData
# Create a custom signer class implementing BaseSigner interface
class CustomSigner(BaseSigner):
@property
def public_key(self) -> int:
return 0x123
def sign_transaction(self, transaction: Transaction) -> List[int]:
return [0x0, 0x1]
def sign_message(
self, typed_data: TypedData, account_address: int
) -> List[int]:
return [0x0, 0x1]
# Create an Account instance with the signer you've implemented
custom_signer = CustomSigner()
client = FullNodeClient(node_url="https://your.node.url")
account = Account(
client=client,
address=0x1111,
signer=custom_signer,
chain=StarknetChainId.SEPOLIA,
)
# Now you can use Account as you'd always do
Signing with Ledger#
LedgerSigner allows you to sign transactions using a Ledger device. The device must be unlocked and Starknet app needs to be open.
Currently used version of Starknet app is 2.3.1
.
from starknet_py.net.signer.ledger_signer import LedgerSigner, LedgerSigningMode
# Create a `LedgerSigner` instance and pass chain id
signer = LedgerSigner(
chain_id=StarknetChainId.SEPOLIA,
)
# Sign the transaction
signature = signer.sign_transaction(transaction)
# Ledger also allows to blind sign transactions, but keep in mind that blind signing
# is not recommended. It's unsafe because it lets you approve transactions or
# messages without seeing their full contents.
# ⚠️ Use blind signing at your own risk
signer.signing_mode = LedgerSigningMode.BLIND
signature = signer.sign_transaction(transaction)
client = FullNodeClient(node_url="https://your.node.url")
# Create an `Account` instance with the ledger signer
account = Account(
client=client,
address=0x1111,
signer=signer,
chain=StarknetChainId.SEPOLIA,
)
# Now you can use Account as you'd always do
Deploying account and transferring STRK#
from starknet_py.contract import Contract
from starknet_py.hash.address import compute_address
from starknet_py.net.account.account import Account
from starknet_py.net.full_node_client import FullNodeClient
from starknet_py.net.signer.ledger_signer import LedgerSigner
rpc_client = FullNodeClient(node_url="https://your.node.url")
signer = LedgerSigner(
chain_id=StarknetChainId.SEPOLIA,
)
# argent v0.4.0 class hash
class_hash = 0x36078334509B514626504EDC9FB252328D1A240E4E948BEF8D0C08DFF45927F
salt = 1
calldata = [0, signer.public_key, 1]
address = compute_address(
salt=salt,
class_hash=class_hash,
constructor_calldata=calldata,
)
account = Account(
address=address,
client=rpc_client,
signer=signer,
chain=StarknetChainId.SEPOLIA,
)
# Remember to prefund the account
signed_tx = await account.sign_deploy_account_v3(
class_hash=class_hash,
contract_address_salt=salt,
constructor_calldata=calldata,
auto_estimate=True,
)
await rpc_client.deploy_account(signed_tx)
recipient_address = 0x123
contract = await Contract.from_address(
provider=account, address=STRK_FEE_CONTRACT_ADDRESS
)
invocation = await contract.functions["transfer"].invoke_v3(
recipient_address, 100, auto_estimate=True
)
await invocation.wait_for_acceptance()
Signing off-chain messages#
Account lets you sign an off-chain message by using encoding standard proposed here.
You can also verify a message, which is done by a call to is_valid_signature
endpoint in the account’s contract (e.g. OpenZeppelin’s account contract).
from starknet_py.net.account.account import Account
from starknet_py.net.full_node_client import FullNodeClient
from starknet_py.net.models import StarknetChainId
from starknet_py.net.signer.key_pair import KeyPair
from starknet_py.utils.typed_data import TypedData
# Create a TypedData dictionary
typed_data = {
"types": {
"StarkNetDomain": [
{"name": "name", "type": "felt"},
{"name": "version", "type": "felt"},
{"name": "chainId", "type": "felt"},
],
"Person": [
{"name": "name", "type": "felt"},
{"name": "wallet", "type": "felt"},
],
"Mail": [
{"name": "from", "type": "Person"},
{"name": "to", "type": "Person"},
{"name": "contents", "type": "felt"},
],
},
"primaryType": "Mail",
"domain": {"name": "StarkNet Mail", "version": "1", "chainId": "1"},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
},
"contents": "Hello, Bob!",
},
}
# Create an Account instance
client = FullNodeClient(node_url="https://your.node.url")
account = Account(
client=client,
address="0x1111",
key_pair=KeyPair(private_key=123, public_key=456),
chain=StarknetChainId.SEPOLIA,
)
# Sign the message
signature = account.sign_message(typed_data=typed_data)
# Verify the message
verify_result = account.verify_message(typed_data=typed_data, signature=signature)
# Or if just a message hash is needed
data = TypedData.from_dict(typed_data)
message_hash = data.message_hash(account.address)
Signing for fee estimation#
Account allows signing transactions for the purpose of fee estimation. Transactions signed for fee estimation use a transaction version that makes them non-executable on Starknet. If a transaction like this was to be intercepted in transport, it could not be executed without the user consent.
Attention
Conventionally signed transactions can still be used to estimate fee. They however don’t offer the extra security of signing specifically for the purpose of fee estimation.
When manually estimating fee for transactions, always prefer estimation specific signing.
# Create a transaction
call = map_contract.functions["put"].prepare_invoke_v3(key=10, value=20)
transaction = await account.sign_invoke_v3(
calls=call,
resource_bounds=MAX_RESOURCE_BOUNDS,
)
# Re-sign a transaction for fee estimation
estimate_transaction = await account.sign_for_fee_estimate(transaction)
# Transaction uses a version that cannot be executed on Starknet
assert estimate_transaction.version == 3 + 2**128
assert estimate_transaction.signature != transaction.signature
# Get a fee estimation
estimate = await account.client.estimate_fee(transaction)
assert estimate.overall_fee > 0
print(estimate.overall_fee)
print(estimate.to_resource_bounds())
# Use a new fee in original transaction
transaction = await account.sign_invoke_v3(
calls=call, resource_bounds=estimate.to_resource_bounds()
)
# Send a transaction
result = await account.client.send_transaction(transaction)
await account.client.wait_for_tx(result.transaction_hash)