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 1.1.1 and only blind-signing is possible. Clear-signing will be available in the near future.


# Create a `LedgerSigner` instance with the derivation path and chain id
signer = LedgerSigner(
    derivation_path_str="m/2645'/1195502025'/1470455285'/0'/0'/0",
    chain_id=StarknetChainId.SEPOLIA,
)

# Sign the transaction
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#

class_hash = 0x61DAC032F228ABEF9C6626F995015233097AE253A7F72D68552DB02F2971B8F
salt = 1
calldata = [signer.public_key]
address = compute_address(
    salt=salt,
    class_hash=class_hash,
    constructor_calldata=calldata,
)
account = Account(
    address=address,
    client=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 client.deploy_account(signed_tx)

recipient_address = (
    0x1323CACBC02B4AAED9BB6B24D121FB712D8946376040990F2F2FA0DCF17BB5B
)
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.stark_curve_signer 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_v1(key=10, value=20)
transaction = await account.sign_invoke_v1(calls=call, max_fee=0)

# 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 == 1 + 2**128
assert estimate_transaction.signature != transaction.signature

# Get a fee estimation
estimate = await account.client.estimate_fee(transaction)
assert estimate.overall_fee > 0

# Use a new fee in original transaction
transaction = await account.sign_invoke_v1(calls=call, max_fee=estimate.overall_fee)

# Send a transaction
result = await account.client.send_transaction(transaction)
await account.client.wait_for_tx(result.transaction_hash)