Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,3 @@ thumbs.db
.ruff_cache/

# Generated protobuf files
**/src/**/proto
18 changes: 16 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
.PHONY: setup prebuild test lint format clean help
.PHONY: setup prebuild test lint format clean help test-app

# Default target
help:
@echo "Available targets:"
@echo " setup - Complete setup (install dependencies and run prebuild)"
@echo " prebuild - Run prebuild for all packages"
@echo " test - Run all tests"
@echo " test-app - Run test application (interactive)"
@echo " lint - Run linting checks"
@echo " format - Format code with black"
@echo " clean - Clean generated files"
Expand All @@ -14,7 +15,11 @@ help:
setup:
@echo "Setting up Cypherock SDK..."
@echo "1. Installing dependencies..."
poetry install
@HOMEBREW_PREFIX=$$([ -d "/opt/homebrew" ] && echo "/opt/homebrew" || echo "/usr/local") && \
export LIBRARY_PATH="$$HOMEBREW_PREFIX/lib:$$LIBRARY_PATH" && \
export CPPFLAGS="-I$$HOMEBREW_PREFIX/include $$CPPFLAGS" && \
export LDFLAGS="-L$$HOMEBREW_PREFIX/lib $$LDFLAGS" && \
poetry install
@echo "2. Running prebuild..."
$(MAKE) prebuild
@echo "Setup complete!"
Expand Down Expand Up @@ -42,6 +47,15 @@ test: prebuild
@echo "Running util package tests..."
poetry run pytest packages/util/tests/ -v

# Run test application (for testing with actual firmware)
test-app: prebuild
@echo "Running test application..."
@echo "Use 'poetry run python test_app/main.py --help' for usage"
@HOMEBREW_PREFIX=$$([ -d "/opt/homebrew" ] && echo "/opt/homebrew" || echo "/usr/local") && \
export DYLD_LIBRARY_PATH="$$HOMEBREW_PREFIX/lib:$$DYLD_LIBRARY_PATH" && \
export LD_LIBRARY_PATH="$$HOMEBREW_PREFIX/lib:$$LD_LIBRARY_PATH" && \
poetry run python test_app/main.py --list-devices || true

# Run linting
lint:
@echo "Running linting checks..."
Expand Down
1 change: 1 addition & 0 deletions packages/app_btc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/app_btc/proto/generated
366 changes: 342 additions & 24 deletions packages/app_btc/poetry.lock

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions packages/app_btc/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,23 @@ readme = "README.md"
packages = [{include = "src/app_btc"}]

[tool.poetry.dependencies]
python = ">=3.11"
python = ">=3.11,<4"
# Bitcoin libraries
bitcoinlib = "^0.7.5"
python-bitcointx = "^1.1.4"
coincurve = "^20.0.0"
base58 = "^2.1.1"
ecdsa = "^0.18.0"
ecdsa = "^0.19"
bech32 = "^1.2.0"
# HTTP client
httpx = "^0.24.0"
# Cryptography
cryptography = "^41.0.0"
# Protobuf
betterproto = "^1.2.5"
protobuf = "^4.24.0"
protobuf = "^6.0.0"
semver = "^3.0.4"
bitcoin-utils = "^0.7.3"


[tool.poetry.group.dev.dependencies]
Expand All @@ -42,7 +43,7 @@ line-length = 88
target-version = ['py311']

[tool.mypy]
python_version = "3.11"
python_version = "^3.11"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true
Expand Down
16 changes: 8 additions & 8 deletions packages/app_btc/scripts/prebuild.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ cd "$(dirname "$0")/.."
rm -rf ./src/app_btc/proto/generated/*.py || true
rm -rf ./src/app_btc/proto/generated/btc || true

# Create output directory for generated files
mkdir -p src/app_btc/proto/generated

# Use poetry run python3 to ensure we use the root environment with betterproto
# Use poetry run python3 to ensure we use the root environment with standard protoc
PYTHON_CMD="poetry run python3"

protoc --python_betterproto_out=./src/app_btc/proto/generated \
protoc --python_out=./src/app_btc/proto/generated \
--proto_path="../../submodules/common/proto" \
../../submodules/common/proto/btc/core.proto \
../../submodules/common/proto/btc/error.proto \
../../submodules/common/proto/btc/get_public_key.proto \
../../submodules/common/proto/btc/get_xpubs.proto \
../../submodules/common/proto/common.proto

protoc --python_out=./src/app_btc/proto/generated \
--proto_path="../../submodules/common/proto" \
../../submodules/common/proto/btc/sign_txn.proto || echo "Warning: sign_txn.proto generation failed - optional fields not supported"
../../submodules/common/proto/btc/sign_txn.proto \
../../submodules/common/proto/common.proto \
../../submodules/common/proto/error.proto

$PYTHON_CMD ../../scripts/extract_types/__init__.py ./src/app_btc/proto/generated ./src/app_btc/proto/generated/types.py
# Fix imports in generated files
$PYTHON_CMD ../../scripts/fix_proto_imports.py ./src/app_btc/proto/generated btc
1 change: 0 additions & 1 deletion packages/app_btc/src/app_btc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from .app import BtcApp
from .proto.generated.types import *
from .operations.types import *
from .utils import (
update_logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from util.utils import create_status_listener, create_logger_with_prefix
from util.utils.assert_utils import assert_condition
from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType
from ...proto.generated.btc import GetPublicKeyStatus
from ...proto.generated.common import SeedGenerationStatus
from ...proto.generated.btc.get_public_key_pb2 import GetPublicKeyStatus
from ...proto.generated.common_pb2 import SeedGenerationStatus
from ...utils import (
assert_or_throw_invalid_result,
OperationHelper,
Expand Down Expand Up @@ -71,7 +71,7 @@ async def get_public_key(

helper = OperationHelper(
sdk=sdk,
query_key="getPublicKey",
query_key="get_public_key",
result_key="get_public_key",
on_status=on_status,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from typing import List
from coincurve import PublicKey
from ....utils import get_bitcoin_py_lib, get_network_from_path, get_purpose_type
from ....utils import get_network_from_path, get_purpose_type
from bitcoinutils.keys import PublicKey as BitcoinPublicKey, P2shAddress
from bitcoinutils.setup import setup


def get_address_from_public_key(uncompressed_public_key: bytes, path: List[int]) -> str:
"""
1. Compress the uncompressed public key using secp256k1
2. Get the appropriate payment function based on the path
3. Generate the address using the payment function
1. Get the purpose type from the derivation path
2. Create the bitcoin public key object from the uncompressed public key
3. Get the address from the public key based on the purpose type
4. Assert that the address was generated successfully

Args:
Expand All @@ -20,28 +21,26 @@ def get_address_from_public_key(uncompressed_public_key: bytes, path: List[int])
Raises:
AssertionError: If address could not be derived
"""
if len(uncompressed_public_key) == 33:
compressed_public_key = uncompressed_public_key
elif len(uncompressed_public_key) == 65:
compressed_public_key = PublicKey(uncompressed_public_key).format(
compressed=True
)
else:
raise ValueError(
f"Invalid public key length: {len(uncompressed_public_key)} bytes. Expected 33 (compressed) or 65 (uncompressed)."
)

bitcoin_py_lib = get_bitcoin_py_lib()
network_config = get_network_from_path(path)
network = "bitcoin" if network_config.pub_key_hash == 0 else "testnet"
network = "mainnet" if network_config.pub_key_hash == 0 else "testnet"
setup(network)

purpose_type = get_purpose_type(path)

if purpose_type == "segwit":
result = bitcoin_py_lib.payments.p2wpkh(compressed_public_key, network)
pubkey = BitcoinPublicKey(uncompressed_public_key.hex())

if purpose_type == "legacy":
address = pubkey.get_address()
elif purpose_type == "segwit":
address = pubkey.get_segwit_address()
elif purpose_type == "nested_segwit":
address = P2shAddress.from_script(
pubkey.get_segwit_address().to_script_pub_key()
)
elif purpose_type == "taproot":
address = pubkey.get_taproot_address()
else:
result = bitcoin_py_lib.payments.p2pkh(compressed_public_key, network)
raise ValueError(f"Unsupported purpose type: {purpose_type}")

address = result["address"]
assert address, "Could not derive address"
return address
return address.to_string()
20 changes: 15 additions & 5 deletions packages/app_btc/src/app_btc/operations/getXpubs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from core.types import ISDK
from util.utils import create_status_listener, create_logger_with_prefix
from util.utils.assert_utils import assert_condition
from ...proto.generated.btc import GetXpubsStatus, GetXpubsResultResponse
from ...proto.generated.common import SeedGenerationStatus
from ...proto.generated.btc.get_xpubs_pb2 import GetXpubsStatus, GetXpubsResultResponse
from ...proto.generated.common_pb2 import SeedGenerationStatus
from ...utils import (
assert_or_throw_invalid_result,
OperationHelper,
logger as root_logger,
configure_app_id,
assert_derivation_path,
get_purpose_type,
)
from .types import GetXpubsEvent, GetXpubsParams

Expand Down Expand Up @@ -69,8 +70,8 @@ async def get_xpubs(

helper = OperationHelper(
sdk=sdk,
query_key="getXpubs",
result_key="getXpubs",
query_key="get_xpubs",
result_key="get_xpubs",
on_status=on_status,
)

Expand All @@ -89,4 +90,13 @@ async def get_xpubs(

force_status_update(GetXpubsEvent.PIN_CARD)

return GetXpubsResultResponse(xpubs=result.result.xpubs)
return GetXpubsResultResponse(
xpubs=[
(
f"tr({xpub})"
if get_purpose_type(params.derivation_paths[i]["path"]) == "taproot"
else xpub
)
for i, xpub in enumerate(result.result.xpubs)
]
)
35 changes: 22 additions & 13 deletions packages/app_btc/src/app_btc/operations/signTxn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
hex_to_uint8array,
uint8array_to_hex,
)
from ...proto.generated.btc import SignTxnStatus
from ...proto.generated.common import SeedGenerationStatus
from ...proto.generated.btc.sign_txn_pb2 import SignTxnStatus
from ...proto.generated.common_pb2 import SeedGenerationStatus
from ...utils import (
assert_or_throw_invalid_result,
OperationHelper,
Expand All @@ -18,6 +18,7 @@
AppFeatures,
address_to_script_pub_key,
create_signed_transaction,
get_purpose_type,
)
from ...services.transaction import get_raw_txn_hash
from .helpers import assert_sign_txn_params
Expand Down Expand Up @@ -75,8 +76,8 @@ async def sign_txn(

helper = OperationHelper(
sdk=sdk,
query_key="signTxn",
result_key="signTxn",
query_key="sign_txn",
result_key="sign_txn",
on_status=on_status,
)

Expand All @@ -100,7 +101,12 @@ async def sign_txn(
"locktime": params.txn.locktime or SIGN_TXN_DEFAULT_PARAMS["locktime"],
"input_count": len(params.txn.inputs),
"output_count": len(params.txn.outputs),
"sighash": params.txn.hash_type or SIGN_TXN_DEFAULT_PARAMS["hashtype"],
"sighash": params.txn.hash_type
or (
0
if get_purpose_type(params.derivation_path) == "taproot"
else SIGN_TXN_DEFAULT_PARAMS["hashtype"]
),
}
}
)
Expand All @@ -112,12 +118,15 @@ async def sign_txn(
for i, input_data in enumerate(params.txn.inputs):
prev_txn_hash = bytes.fromhex(input_data.prev_txn_id)[::-1].hex()

prev_txn = input_data.prev_txn or await get_raw_txn_hash(
{
"hash": input_data.prev_txn_id,
"coin_type": get_coin_type_from_path(params.derivation_path),
}
)
if input_data.prev_txn is not None:
prev_txn = input_data.prev_txn
else:
prev_txn = get_raw_txn_hash(
{
"hash": input_data.prev_txn_id,
"coinType": get_coin_type_from_path(params.derivation_path),
}
)
inputs[i].prev_txn = prev_txn

await helper.send_query(
Expand All @@ -130,7 +139,7 @@ async def sign_txn(
input_data.address, params.derivation_path
)
),
"value": input_data.value,
"value": int(input_data.value),
"sequence": input_data.sequence
or SIGN_TXN_DEFAULT_PARAMS["input"]["sequence"],
"change_index": input_data.change_index,
Expand All @@ -156,7 +165,7 @@ async def sign_txn(
output.address, params.derivation_path
)
),
"value": output.value,
"value": int(output.value),
"is_change": output.is_change,
"changes_index": output.address_index,
}
Expand Down
2 changes: 1 addition & 1 deletion packages/app_btc/src/app_btc/operations/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
GetXpubsEventHandler,
GetXpubsParams,
)
from ..proto.generated.btc import GetXpubsResultResponse
from ..proto.generated.btc.get_xpubs_pb2 import GetXpubsResultResponse
from .signTxn.types import (
SignTxnEvent,
SignTxnEventHandler,
Expand Down
6 changes: 3 additions & 3 deletions packages/app_btc/src/app_btc/services/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
base_url = "/v2/transaction"


async def get_raw_txn_hash(params: Dict[str, str]) -> str:
def get_raw_txn_hash(params: Dict[str, str]) -> str:
"""
Get raw transaction hash from the API.

Expand All @@ -15,5 +15,5 @@ async def get_raw_txn_hash(params: Dict[str, str]) -> str:
Raw transaction hex string

"""
response = await http.post(f"{base_url}/hex", json=params)
return response.json()["data"]["data"]
response = http.post(f"{base_url}/hex", json=params)
return response.json()["data"]
2 changes: 1 addition & 1 deletion packages/app_btc/src/app_btc/utils/assert_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import TypeVar, Optional
from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType
from util.utils.assert_utils import assert_condition
from ..proto.generated.error import CommonError
from app_btc.proto.generated.error_pb2 import CommonError

T = TypeVar("T")

Expand Down
Loading