Merge pull request #548 from LedgerHQ/release/1.10.4
App release 1.10.4
1
.github/workflows/auto-author-assign.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Auto Author Assign'
|
||||
|
||||
on:
|
||||
|
||||
34
.github/workflows/build-workflow.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Compilation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
nano_release_build:
|
||||
name: Build release application for NanoS, X and S+
|
||||
strategy:
|
||||
matrix:
|
||||
sdk: ["$NANOS_SDK", "$NANOX_SDK", "$NANOSP_SDK"]
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Ethereum
|
||||
run: |
|
||||
make -j BOLOS_SDK=${{ matrix.sdk }}
|
||||
|
||||
- name: Build an altcoin
|
||||
run: |
|
||||
make clean
|
||||
make -j BOLOS_SDK=${{ matrix.sdk }} CHAIN=polygon
|
||||
93
.github/workflows/ci-workflow.yml
vendored
@@ -1,39 +1,19 @@
|
||||
---
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
scan-build:
|
||||
name: Clang Static Analyzer
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build with Clang Static Analyzer
|
||||
run: |
|
||||
make clean
|
||||
scan-build --use-cc=clang -analyze-headers -enable-checker security -enable-checker unix -enable-checker valist -o scan-build --status-bugs make default
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: scan-build
|
||||
path: scan-build
|
||||
|
||||
# =====================================================
|
||||
# ZEMU TESTS
|
||||
# =====================================================
|
||||
# =====================================================
|
||||
# ZEMU TESTS
|
||||
# =====================================================
|
||||
|
||||
building_for_e2e_zemu_tests:
|
||||
name: Building binaries for E2E Zemu tests
|
||||
@@ -97,9 +77,9 @@ jobs:
|
||||
- name: Run zemu tests
|
||||
run: cd tests/zemu/ && yarn test
|
||||
|
||||
# =====================================================
|
||||
# SPECULOS TESTS
|
||||
# =====================================================
|
||||
# =====================================================
|
||||
# SPECULOS TESTS
|
||||
# =====================================================
|
||||
|
||||
|
||||
building_for_e2e_speculos_tests:
|
||||
@@ -114,9 +94,9 @@ jobs:
|
||||
- name: Build testing binaries
|
||||
run: |
|
||||
mkdir tests/speculos/elfs
|
||||
make clean && make -j DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOS_SDK && mv bin/app.elf tests/speculos/elfs/nanos.elf
|
||||
make clean && make -j DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOX_SDK && mv bin/app.elf tests/speculos/elfs/nanox.elf
|
||||
make clean && make -j DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOSP_SDK && mv bin/app.elf tests/speculos/elfs/nanosp.elf
|
||||
make clean && make -j DEBUG=1 NFT_STAGING_KEY=1 BOLOS_SDK=$NANOS_SDK && mv bin/app.elf tests/speculos/elfs/nanos.elf
|
||||
make clean && make -j DEBUG=1 NFT_STAGING_KEY=1 BOLOS_SDK=$NANOX_SDK && mv bin/app.elf tests/speculos/elfs/nanox.elf
|
||||
make clean && make -j DEBUG=1 NFT_STAGING_KEY=1 BOLOS_SDK=$NANOSP_SDK && mv bin/app.elf tests/speculos/elfs/nanosp.elf
|
||||
|
||||
- name: Upload app binaries
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -162,16 +142,16 @@ jobs:
|
||||
pytest --model ${{ matrix.model }} --path ./elfs/${{ matrix.model }}.elf --display headless
|
||||
|
||||
|
||||
# =====================================================
|
||||
# RAGGER TESTS
|
||||
# =====================================================
|
||||
# =====================================================
|
||||
# RAGGER TESTS
|
||||
# =====================================================
|
||||
|
||||
build_ragger_elfs:
|
||||
name: Build app for Ragger tests
|
||||
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1
|
||||
with:
|
||||
upload_app_binaries_artifact: "ragger_elfs"
|
||||
flags: "DEBUG=1 CAL_CI_KEY=1 DOMAIN_NAME_TEST_KEY=1"
|
||||
flags: "DEBUG=1 CAL_TEST_KEY=1 DOMAIN_NAME_TEST_KEY=1 SET_PLUGIN_TEST_KEY=1 NFT_TEST_KEY=1"
|
||||
|
||||
jobs-ragger-tests:
|
||||
name: Run Ragger tests
|
||||
@@ -179,4 +159,43 @@ jobs:
|
||||
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1
|
||||
with:
|
||||
download_app_binaries_artifact: "ragger_elfs"
|
||||
test_dir: tests/ragger
|
||||
|
||||
# =====================================================
|
||||
# STATIC ANALYSIS
|
||||
# =====================================================
|
||||
|
||||
# Static analysis on the main ETH chain is covered by the guidelines enforcer
|
||||
scan-build:
|
||||
name: Clang Static Analyzer on altcoin
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
device: ["nanos", "nanos2", "nanox", "stax"]
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Build with Clang Static Analyzer
|
||||
run: |
|
||||
eval "BOLOS_SDK=\$$(echo ${{ matrix.device }} | tr [:lower:] [:upper:])_SDK" && \
|
||||
echo "BOLOS_SDK value will be: ${BOLOS_SDK}" && \
|
||||
make -j ENABLE_SDK_WERROR=1 BOLOS_SDK=${BOLOS_SDK} CHAIN=polygon scan-build
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: scan-build
|
||||
path: scan-build
|
||||
|
||||
- name: Upload scan result
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: scan-build
|
||||
path: scan-build
|
||||
|
||||
36
.github/workflows/lint-workflow.yml
vendored
@@ -1,26 +1,26 @@
|
||||
---
|
||||
name: Code style check
|
||||
|
||||
# This workflow will run linting checks to ensure a level of uniformization among all Ledger applications.
|
||||
#
|
||||
# The presence of this workflow is mandatory as a minimal level of linting is required.
|
||||
# You are however free to modify the content of the .clang-format file and thus the coding style of your application.
|
||||
# We simply ask you to not diverge too much from the linting of the Boilerplate application.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
job_lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Lint
|
||||
uses: DoozyX/clang-format-lint-action@v0.14
|
||||
with:
|
||||
source: "./"
|
||||
extensions: "h,c"
|
||||
clangFormatVersion: 12.0.1
|
||||
check_linting:
|
||||
name: Check linting using the reusable workflow
|
||||
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_lint.yml@v1
|
||||
with:
|
||||
source: './'
|
||||
extensions: 'h,c'
|
||||
version: 12
|
||||
|
||||
202
.github/workflows/pr_on_all_plugins.yml
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
name: Open PRs in Ethereum plugins to update SDK
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# TODO add auto trigger once finalized
|
||||
|
||||
jobs:
|
||||
build-repositories-matrix:
|
||||
name: Build repo matrix
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
|
||||
steps:
|
||||
- name: List plugin repositories
|
||||
id: list-repos
|
||||
run: |
|
||||
# Retrieve the list of repositories from LedgerHQ organization
|
||||
raw_repo_list=$(gh repo list LedgerHQ -L 2000 \
|
||||
--json name \
|
||||
--json isPrivate \
|
||||
--json isArchived \
|
||||
--jq '.[] | select(.isPrivate == false and .isArchived == false and select(.name | startswith("app-plugin-"))) | .name')
|
||||
# Format the repository list as a JSON array
|
||||
formatted_repo_list="["
|
||||
while IFS= read -r repo; do
|
||||
formatted_repo_list+="\"$repo\", "
|
||||
done < <(echo "$raw_repo_list")
|
||||
formatted_repo_list=${formatted_repo_list%%, }"]"
|
||||
echo "Formatted Repository List: $formatted_repo_list"
|
||||
# Set output
|
||||
echo "repo_list=$formatted_repo_list" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
repo-matrix: ${{ steps.list-repos.outputs.repo_list }}
|
||||
|
||||
|
||||
get-sdk-ref:
|
||||
name: Get latest SDK reference
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout SDK repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: LedgerHQ/ethereum-plugin-sdk
|
||||
ref: develop
|
||||
- name: Retrieve SDK reference
|
||||
id: get-hash
|
||||
run: |
|
||||
# Get the short SHA reference of the latest commit in the SDK repository
|
||||
sdk_ref=$(git rev-parse --short HEAD)
|
||||
echo "SDK Reference: $sdk_ref"
|
||||
# Set output
|
||||
echo "sdk_ref=$sdk_ref" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
sdk_ref: ${{ steps.get-hash.outputs.sdk_ref }}
|
||||
|
||||
|
||||
open-issue:
|
||||
name: Open SDK update summary issue
|
||||
runs-on: ubuntu-latest
|
||||
needs: get-sdk-ref
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
|
||||
steps:
|
||||
- name: Create 'auto' label if missing
|
||||
run: |
|
||||
if [[ -z $(gh label list --repo ${{ github.repository }} --search auto) ]]; then
|
||||
gh label create 'auto' --repo ${{ github.repository }} --color 'b4a8d1' --description 'Automatically created'
|
||||
fi
|
||||
- name: Open SDK Update Issue
|
||||
id: open-issue
|
||||
run: |
|
||||
# Create a new issue with the SDK reference in the SDK repository
|
||||
issue_url=$(gh issue create \
|
||||
--repo ${{ github.repository }} \
|
||||
--label auto \
|
||||
--title "Bumping Eth plugin SDK to latest version ${{ needs.get-sdk-ref.outputs.sdk_ref }}" \
|
||||
--body "Placeholder")
|
||||
echo "Issue URL: $issue_url"
|
||||
# Set output
|
||||
echo "issue_url=$issue_url" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
issue_url: ${{ steps.open-issue.outputs.issue_url }}
|
||||
|
||||
|
||||
open-prs:
|
||||
name: Open pull requests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-repositories-matrix, open-issue, get-sdk-ref]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
repo: ${{ fromJSON(needs.build-repositories-matrix.outputs.repo-matrix) }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout plugin repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: LedgerHQ/${{ matrix.repo }}
|
||||
submodules: recursive
|
||||
ref: develop
|
||||
# by default the action uses fetch-depth = 1, which creates
|
||||
# shallow repositories from which we can't push
|
||||
fetch-depth: 0
|
||||
# needed, else the push inside the action will use default credentials
|
||||
# instead of provided ones
|
||||
persist-credentials: false
|
||||
|
||||
- name: Update submodule
|
||||
run: |
|
||||
cd ethereum-plugin-sdk/
|
||||
git checkout ${{ needs.get-sdk-ref.outputs.sdk_ref }}
|
||||
|
||||
- name: Commit changes
|
||||
id: commit-changes
|
||||
run: |
|
||||
# Set credentials for commit creation
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
# Branch name and title will be unique by design
|
||||
branch_name="auto/bump_sdk_to_${{ needs.get-sdk-ref.outputs.sdk_ref }}"
|
||||
title="[auto-update] Bump SDK to latest develop version ${{ needs.get-sdk-ref.outputs.sdk_ref }}"
|
||||
echo "Branch Name: $branch_name"
|
||||
echo "Title: $title"
|
||||
git status
|
||||
git commit -am "$title"
|
||||
# Set output
|
||||
echo "title=$title" >> $GITHUB_OUTPUT
|
||||
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push commit
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.CI_BOT_TOKEN }}
|
||||
branch: ${{ steps.commit-changes.outputs.branch_name }}
|
||||
repository: LedgerHQ/${{ matrix.repo }}
|
||||
force: true
|
||||
|
||||
- name: Create 'auto' label if missing
|
||||
run: |
|
||||
if [[ -z $(gh label list --search auto) ]]; then
|
||||
gh label create 'auto' --color 'b4a8d1' --description 'Automatically created'
|
||||
fi
|
||||
|
||||
- name: Create pull request and commment on SDK issue
|
||||
run: |
|
||||
# Github limits the number of possible PR being opened in a given time window.
|
||||
# The limits are 20 creation per minute and 150 per hour.
|
||||
# As suggested in the Github documentation, put a sleep between each POST call
|
||||
# 3 seconds should be sufficient but let's sleep 4 seconds just in case.
|
||||
sleep $((4 * ${{ strategy.job-index }}))
|
||||
# Create the PR with a placeholder body. Will be consolidated at a later step
|
||||
pr_url=$(gh pr create \
|
||||
--base 'develop' \
|
||||
--head '${{ steps.commit-changes.outputs.branch_name }}' \
|
||||
--label 'auto' \
|
||||
--title '${{ steps.commit-changes.outputs.title }}' \
|
||||
--body 'Created by a Github workflow')
|
||||
echo "Pull request URL: $pr_url"
|
||||
# Log the url of the PR in the issue on SDK side. We'll collect them from the issue later
|
||||
gh issue comment "${{ needs.open-issue.outputs.issue_url }}" --body "OPENED $pr_url"
|
||||
|
||||
|
||||
clean-issue:
|
||||
name: Clean SDK update summary issue
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-sdk-ref, open-issue, open-prs]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
|
||||
steps:
|
||||
- name: Collect all comments on the SDK issue
|
||||
run: |
|
||||
# Get the full text of the issue: metadata + title + rich comments
|
||||
content="$(gh issue view --comments "${{ needs.open-issue.outputs.issue_url }}")"
|
||||
# New header of the issue body
|
||||
header="Bumping Ethereum plugin SDK to latest version ${{ needs.get-sdk-ref.outputs.sdk_ref }} for all Ethereum plugins:"
|
||||
# Filter the full text of the issue to collect only the PR urls
|
||||
lines=""
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ "OPENED" ]]; then
|
||||
lines+=$(echo "$line" | cut -d ' ' -f2)$'\n'
|
||||
fi
|
||||
done < <(echo "$content")
|
||||
# Use print to resolve the '\n' chars
|
||||
new_body="$(printf "$header\n$lines")"
|
||||
echo "New issue body: $new_body"
|
||||
# Set the consolidated body of the issue
|
||||
gh issue edit "${{ needs.open-issue.outputs.issue_url }}" --body "$new_body"
|
||||
|
||||
- name: Clean comments on the SDK issue
|
||||
run: |
|
||||
# gh api uses id instead of url
|
||||
issue_id="$(basename "${{ needs.open-issue.outputs.issue_url }}")"
|
||||
# Get url of all comments on the issue
|
||||
comment_urls="$(gh api "repos/${{ github.repository }}/issues/${issue_id}/comments" --jq '.[].url')"
|
||||
# Delete each comment using the Github REST api
|
||||
while IFS= read -r url; do
|
||||
echo "Deleting comment: $comment_urls"
|
||||
gh api -X DELETE "$url"
|
||||
done < <(echo "$comment_urls")
|
||||
80
.github/workflows/python-client.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: Python client checks, package build and deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'client-*'
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
paths:
|
||||
- client/**
|
||||
- .github/workflows/python-client.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- client/**
|
||||
- .github/workflows/python-client.yml
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Linting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v3
|
||||
- run: pip install flake8 flake8-pyproject
|
||||
- name: Flake8 lint Python code
|
||||
run: (cd client && flake8 src/)
|
||||
|
||||
mypy:
|
||||
name: Type checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v3
|
||||
- run: pip install mypy
|
||||
- name: Mypy type checking
|
||||
run: (cd client && mypy src/)
|
||||
|
||||
package_and_deploy:
|
||||
name: Build and deploy Ethereum Client Python package
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, mypy]
|
||||
steps:
|
||||
|
||||
- name: Clone
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build Python package
|
||||
run: |
|
||||
pip install --upgrade pip build twine
|
||||
cd client/
|
||||
python -m build;
|
||||
python -m twine check dist/*
|
||||
pip install .;
|
||||
echo "TAG_VERSION=$(python -c 'from ledger_app_clients.ethereum import __version__; print(__version__)')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Check version against CHANGELOG
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
CHANGELOG_VERSION=$(grep -Po '(?<=## \[)(\d+\.)+[^\]]' client/CHANGELOG.md | head -n 1)
|
||||
if [ "${{ env.TAG_VERSION }}" == "${CHANGELOG_VERSION}" ];
|
||||
then
|
||||
echo 'Package and CHANGELOG versions match!';
|
||||
exit 0;
|
||||
else
|
||||
echo "Tag '${{ env.TAG_VERSION }}' and CHANGELOG '${CHANGELOG_VERSION}' versions mismatch!";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
- name: Publish Python package on pypi.org
|
||||
if: success() && github.event_name == 'push'
|
||||
run: (cd client && python -m twine upload --verbose dist/*)
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PUBLIC_API_TOKEN }}
|
||||
TWINE_NON_INTERACTIVE: 1
|
||||
3
.github/workflows/sdk-generation.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Updating the SDK
|
||||
|
||||
on:
|
||||
@@ -25,7 +26,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build new SDK
|
||||
run: python tools/build_sdk.py
|
||||
run: ./tools/build_sdk.sh
|
||||
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
|
||||
5
.github/workflows/swap-ci-workflow.yml
vendored
@@ -1,11 +1,12 @@
|
||||
---
|
||||
name: Swap functional tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
2
.gitignore
vendored
@@ -9,7 +9,7 @@ build/
|
||||
|
||||
# Python
|
||||
*.pyc
|
||||
|
||||
__version__.py
|
||||
|
||||
# JS
|
||||
tests/node_modules
|
||||
|
||||
33
CHANGELOG.md
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [1.10.4](https://github.com/ledgerhq/app-ethereum/compare/1.10.3...1.10.4) - 2023-03-08
|
||||
|
||||
### Added
|
||||
|
||||
- Addresses in EIP-712 messages can now be displayed as a token ticker or a trusted domain name if a match is found
|
||||
- Stax app now has icons of the other supported EVM chains
|
||||
- (network) Bitcichain
|
||||
- (network) Core
|
||||
- (network) Bitrock Mainnet
|
||||
- (network) Numbers Protocol
|
||||
- (network) Linea
|
||||
- (network) Holesky
|
||||
|
||||
### Removed
|
||||
|
||||
- Starkware support
|
||||
- (clone) kUSD
|
||||
- (clone) Tobalaba
|
||||
|
||||
### Changed
|
||||
|
||||
- Can now clear-sign NFT operations on other EVM chains without a clone app
|
||||
- Can now swap on other EVM chains without a clone app
|
||||
- Improved RAM usage
|
||||
- Now shows an explicit ??? ticker when it is unknown instead of falling back to the native chain ticker
|
||||
|
||||
### Fixed
|
||||
|
||||
- Refusal of transactions with very large chain IDs even within specs
|
||||
- Refusal of 10 character-long token tickers
|
||||
- (network) Wanchain chain ID
|
||||
- (network) Sepolia chain ID
|
||||
|
||||
## [1.10.3](https://github.com/ledgerhq/app-ethereum/compare/1.10.2...1.10.3) - 2023-07-27
|
||||
|
||||
### Added
|
||||
|
||||
267
Makefile
@@ -16,13 +16,13 @@
|
||||
#*******************************************************************************
|
||||
|
||||
ifeq ($(BOLOS_SDK),)
|
||||
$(error Environment variable BOLOS_SDK is not set)
|
||||
$(error Environment variable BOLOS_SDK is not set)
|
||||
endif
|
||||
|
||||
include $(BOLOS_SDK)/Makefile.defines
|
||||
|
||||
DEFINES_LIB = USE_LIB_ETHEREUM
|
||||
APP_LOAD_PARAMS= --curve secp256k1 $(COMMON_LOAD_PARAMS)
|
||||
APP_LOAD_PARAMS = --curve secp256k1 $(COMMON_LOAD_PARAMS)
|
||||
# Allow the app to use path 45 for multi-sig (see BIP45).
|
||||
APP_LOAD_PARAMS += --path "45'"
|
||||
# Samsung temporary implementation for wallet ID on 0xda7aba5e/0xc1a551c5
|
||||
@@ -32,29 +32,30 @@ APP_LOAD_PARAMS += --path "1517992542'/1101353413'"
|
||||
# Define Version #
|
||||
##################
|
||||
|
||||
APPVERSION_M=1
|
||||
APPVERSION_N=10
|
||||
APPVERSION_P=3
|
||||
APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)
|
||||
APP_LOAD_FLAGS= --appFlags 0xa40 --dep Ethereum:$(APPVERSION)
|
||||
APPVERSION_M = 1
|
||||
APPVERSION_N = 10
|
||||
APPVERSION_P = 4
|
||||
APPVERSION = $(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)
|
||||
APP_LOAD_FLAGS = --appFlags 0xa40 --dep Ethereum:$(APPVERSION)
|
||||
|
||||
###########################
|
||||
# Set Chain environnement #
|
||||
###########################
|
||||
|
||||
ifeq ($(CHAIN),)
|
||||
CHAIN=ethereum
|
||||
CHAIN = ethereum
|
||||
endif
|
||||
|
||||
SUPPORTED_CHAINS=$(shell find makefile_conf/chain/ -type f -name '*.mk'| sed 's/.*\/\(.*\).mk/\1/g' | sort)
|
||||
SUPPORTED_CHAINS = $(shell find makefile_conf/chain/ -type f -name '*.mk'| sed 's/.*\/\(.*\).mk/\1/g' | sort)
|
||||
|
||||
# Check if chain is available
|
||||
ifeq ($(shell test -s ./makefile_conf/chain/$(CHAIN).mk && echo -n yes), yes)
|
||||
include ./makefile_conf/chain/$(CHAIN).mk
|
||||
include ./makefile_conf/chain/$(CHAIN).mk
|
||||
else
|
||||
$(error Unsupported CHAIN - use $(SUPPORTED_CHAINS))
|
||||
$(error Unsupported CHAIN - use $(SUPPORTED_CHAINS))
|
||||
endif
|
||||
CFLAGS += -DAPPNAME=\"$(APPNAME)\"
|
||||
DEFINES += CHAINID_COINNAME=\"$(TICKER)\" CHAIN_ID=$(CHAIN_ID)
|
||||
|
||||
#########
|
||||
# Other #
|
||||
@@ -65,15 +66,15 @@ DEFINES += $(DEFINES_LIB)
|
||||
|
||||
#prepare hsm generation
|
||||
ifeq ($(TARGET_NAME),TARGET_NANOS)
|
||||
ICONNAME=icons/nanos_app_$(CHAIN).gif
|
||||
ICONNAME = icons/nanos_app_chain_$(CHAIN_ID).gif
|
||||
else ifeq ($(TARGET_NAME),TARGET_STAX)
|
||||
ICONNAME=icons/stax_app_$(CHAIN).gif
|
||||
DEFINES += ICONGLYPH=C_stax_$(CHAIN)_64px
|
||||
DEFINES += ICONBITMAP=C_stax_$(CHAIN)_64px_bitmap
|
||||
DEFINES += ICONGLYPH_SMALL=C_stax_$(CHAIN)
|
||||
GLYPH_FILES += $(ICONNAME)
|
||||
ICONNAME = icons/stax_app_chain_$(CHAIN_ID).gif
|
||||
DEFINES += ICONGLYPH=C_stax_chain_$(CHAIN_ID)_64px
|
||||
DEFINES += ICONBITMAP=C_stax_chain_$(CHAIN_ID)_64px_bitmap
|
||||
DEFINES += ICONGLYPH_SMALL=C_stax_chain_$(CHAIN_ID)
|
||||
GLYPH_FILES += $(ICONNAME)
|
||||
else
|
||||
ICONNAME=icons/nanox_app_$(CHAIN).gif
|
||||
ICONNAME = icons/nanox_app_chain_$(CHAIN_ID).gif
|
||||
endif
|
||||
|
||||
################
|
||||
@@ -85,181 +86,207 @@ all: default
|
||||
# Platform #
|
||||
############
|
||||
|
||||
DEFINES += OS_IO_SEPROXYHAL
|
||||
DEFINES += HAVE_SPRINTF HAVE_SNPRINTF_FORMAT_U
|
||||
DEFINES += HAVE_IO_USB HAVE_L4_USBLIB IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 HAVE_USB_APDU
|
||||
DEFINES += LEDGER_MAJOR_VERSION=$(APPVERSION_M) LEDGER_MINOR_VERSION=$(APPVERSION_N) LEDGER_PATCH_VERSION=$(APPVERSION_P)
|
||||
DEFINES += BUILD_YEAR=\"$(shell date +%Y)\"
|
||||
DEFINES += OS_IO_SEPROXYHAL
|
||||
DEFINES += HAVE_SPRINTF HAVE_SNPRINTF_FORMAT_U
|
||||
DEFINES += HAVE_IO_USB HAVE_L4_USBLIB IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 HAVE_USB_APDU
|
||||
DEFINES += LEDGER_MAJOR_VERSION=$(APPVERSION_M) LEDGER_MINOR_VERSION=$(APPVERSION_N) LEDGER_PATCH_VERSION=$(APPVERSION_P)
|
||||
DEFINES += BUILD_YEAR=\"$(shell date +%Y)\"
|
||||
|
||||
# U2F
|
||||
DEFINES += HAVE_U2F HAVE_IO_U2F
|
||||
DEFINES += U2F_PROXY_MAGIC=\"w0w\"
|
||||
DEFINES += USB_SEGMENT_SIZE=64
|
||||
DEFINES += BLE_SEGMENT_SIZE=32 #max MTU, min 20
|
||||
DEFINES += UNUSED\(x\)=\(void\)x
|
||||
DEFINES += APPVERSION=\"$(APPVERSION)\"
|
||||
DEFINES += HAVE_U2F HAVE_IO_U2F
|
||||
DEFINES += U2F_PROXY_MAGIC=\"w0w\"
|
||||
DEFINES += USB_SEGMENT_SIZE=64
|
||||
DEFINES += BLE_SEGMENT_SIZE=32 #max MTU, min 20
|
||||
DEFINES += APPVERSION=\"$(APPVERSION)\"
|
||||
|
||||
#WEBUSB_URL = www.ledgerwallet.com
|
||||
#DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=$(shell echo -n $(WEBUSB_URL) | wc -c) WEBUSB_URL=$(shell echo -n $(WEBUSB_URL) | sed -e "s/./\\\'\0\\\',/g")
|
||||
#WEBUSB_URL = www.ledgerwallet.com
|
||||
#DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=$(shell echo -n $(WEBUSB_URL) | wc -c) WEBUSB_URL=$(shell echo -n $(WEBUSB_URL) | sed -e "s/./\\\'\0\\\',/g")
|
||||
|
||||
DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=0 WEBUSB_URL=""
|
||||
DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=0 WEBUSB_URL=""
|
||||
|
||||
ifneq (,$(filter $(TARGET_NAME),TARGET_NANOX TARGET_STAX))
|
||||
DEFINES += HAVE_BLE BLE_COMMAND_TIMEOUT_MS=2000
|
||||
DEFINES += HAVE_BLE_APDU # basic ledger apdu transport over BLE
|
||||
SDK_SOURCE_PATH += lib_blewbxx lib_blewbxx_impl
|
||||
DEFINES += HAVE_BLE BLE_COMMAND_TIMEOUT_MS=2000
|
||||
DEFINES += HAVE_BLE_APDU # basic ledger apdu transport over BLE
|
||||
SDK_SOURCE_PATH += lib_blewbxx lib_blewbxx_impl
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128
|
||||
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128
|
||||
else
|
||||
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300
|
||||
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_NAME),TARGET_STAX)
|
||||
DEFINES += NBGL_QRCODE
|
||||
DEFINES += NBGL_QRCODE
|
||||
SDK_SOURCE_PATH += qrcode
|
||||
else
|
||||
DEFINES += HAVE_BAGL
|
||||
DEFINES += HAVE_UX_FLOW
|
||||
ifeq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += HAVE_WALLET_ID_SDK
|
||||
DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=32
|
||||
else
|
||||
DEFINES += HAVE_GLO096
|
||||
DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=64
|
||||
DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature
|
||||
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX
|
||||
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX
|
||||
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX
|
||||
endif
|
||||
DEFINES += HAVE_BAGL
|
||||
DEFINES += HAVE_UX_FLOW
|
||||
ifeq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += HAVE_WALLET_ID_SDK
|
||||
DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=32
|
||||
else
|
||||
DEFINES += HAVE_GLO096
|
||||
DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=64
|
||||
DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature
|
||||
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX
|
||||
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX
|
||||
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX
|
||||
endif
|
||||
endif
|
||||
|
||||
####################
|
||||
# Enabled Features #
|
||||
####################
|
||||
|
||||
# Enables direct data signing without having to specify it in the settings. Useful when testing with speculos.
|
||||
ALLOW_DATA:=0
|
||||
ALLOW_DATA ?= 0
|
||||
ifneq ($(ALLOW_DATA),0)
|
||||
DEFINES += HAVE_ALLOW_DATA
|
||||
DEFINES += HAVE_ALLOW_DATA
|
||||
endif
|
||||
|
||||
# Bypass the signature verification for setExternalPlugin, setPlugin, provideERC20TokenInfo and provideNFTInfo calls
|
||||
BYPASS_SIGNATURES:=0
|
||||
BYPASS_SIGNATURES ?= 0
|
||||
ifneq ($(BYPASS_SIGNATURES),0)
|
||||
DEFINES += HAVE_BYPASS_SIGNATURES
|
||||
DEFINES += HAVE_BYPASS_SIGNATURES
|
||||
endif
|
||||
|
||||
# Enable the SET_PLUGIN test key
|
||||
SET_PLUGIN_TEST_KEY ?= 0
|
||||
ifneq ($(SET_PLUGIN_TEST_KEY),0)
|
||||
DEFINES += HAVE_SET_PLUGIN_TEST_KEY
|
||||
endif
|
||||
|
||||
# NFTs
|
||||
ifneq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += HAVE_NFT_SUPPORT
|
||||
# Enable the NFT testing key
|
||||
NFT_TESTING_KEY:=0
|
||||
ifneq ($(NFT_TESTING_KEY),0)
|
||||
DEFINES += HAVE_NFT_TESTING_KEY
|
||||
DEFINES += HAVE_NFT_SUPPORT
|
||||
NFT_TEST_KEY ?= 0
|
||||
ifneq ($(NFT_TEST_KEY),0)
|
||||
DEFINES += HAVE_NFT_TEST_KEY
|
||||
endif
|
||||
NFT_STAGING_KEY ?= 0
|
||||
ifneq ($(NFT_STAGING_KEY),0)
|
||||
# Key used by the staging backend
|
||||
DEFINES += HAVE_NFT_STAGING_KEY
|
||||
endif
|
||||
endif
|
||||
ifneq (,$(filter $(DEFINES),HAVE_NFT_TEST_KEY))
|
||||
ifneq (, $(filter $(DEFINES),HAVE_NFT_STAGING_KEY))
|
||||
$(error Multiple alternative NFT keys set at once)
|
||||
endif
|
||||
endif
|
||||
|
||||
# Dynamic memory allocator
|
||||
ifneq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += HAVE_DYN_MEM_ALLOC
|
||||
DEFINES += HAVE_DYN_MEM_ALLOC
|
||||
endif
|
||||
|
||||
# EIP-712
|
||||
ifneq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += HAVE_EIP712_FULL_SUPPORT
|
||||
DEFINES += HAVE_EIP712_FULL_SUPPORT
|
||||
endif
|
||||
|
||||
# CryptoAssetsList key
|
||||
CAL_TEST_KEY:=0
|
||||
CAL_CI_KEY:=0
|
||||
CAL_TEST_KEY ?= 0
|
||||
ifneq ($(CAL_TEST_KEY),0)
|
||||
DEFINES += HAVE_CAL_TEST_KEY
|
||||
# Key used in our test framework
|
||||
DEFINES += HAVE_CAL_TEST_KEY
|
||||
endif
|
||||
ifneq ($(CAL_CI_KEY),0)
|
||||
DEFINES += HAVE_CAL_CI_KEY
|
||||
CAL_STAGING_KEY ?= 0
|
||||
ifneq ($(CAL_STAGING_KEY),0)
|
||||
# Key used by the staging CAL
|
||||
DEFINES += HAVE_CAL_STAGING_KEY
|
||||
endif
|
||||
ifneq (,$(filter $(DEFINES),HAVE_CAL_TEST_KEY))
|
||||
ifneq (, $(filter $(DEFINES),HAVE_CAL_STAGING_KEY))
|
||||
# Can't use both the staging and testing keys
|
||||
$(error Multiple alternative CAL keys set at once)
|
||||
endif
|
||||
endif
|
||||
|
||||
# ENS
|
||||
ifneq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += HAVE_DOMAIN_NAME
|
||||
DOMAIN_NAME_TEST_KEY:=0
|
||||
ifneq ($(DOMAIN_NAME_TEST_KEY),0)
|
||||
DEFINES += HAVE_DOMAIN_NAME_TEST_KEY
|
||||
endif
|
||||
DEFINES += HAVE_DOMAIN_NAME
|
||||
DOMAIN_NAME_TEST_KEY ?= 0
|
||||
ifneq ($(DOMAIN_NAME_TEST_KEY),0)
|
||||
DEFINES += HAVE_DOMAIN_NAME_TEST_KEY
|
||||
endif
|
||||
endif
|
||||
|
||||
# Enabling debug PRINTF
|
||||
ifneq ($(DEBUG),0)
|
||||
DEFINES += HAVE_STACK_OVERFLOW_CHECK
|
||||
ifeq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += HAVE_PRINTF PRINTF=screen_printf
|
||||
DEFINES += HAVE_STACK_OVERFLOW_CHECK
|
||||
ifeq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += HAVE_PRINTF PRINTF=screen_printf
|
||||
else
|
||||
DEFINES += HAVE_PRINTF PRINTF=mcu_usb_printf
|
||||
endif
|
||||
else
|
||||
DEFINES += HAVE_PRINTF PRINTF=mcu_usb_printf
|
||||
endif
|
||||
else
|
||||
DEFINES += PRINTF\(...\)=
|
||||
DEFINES += PRINTF\(...\)=
|
||||
endif
|
||||
|
||||
ifneq ($(NOCONSENT),)
|
||||
DEFINES += NO_CONSENT
|
||||
DEFINES += NO_CONSENT
|
||||
endif
|
||||
|
||||
#DEFINES += HAVE_TOKENS_LIST # Do not activate external ERC-20 support yet
|
||||
|
||||
##############
|
||||
# Compiler #
|
||||
##############
|
||||
|
||||
ifneq ($(BOLOS_ENV),)
|
||||
$(info BOLOS_ENV=$(BOLOS_ENV))
|
||||
CLANGPATH := $(BOLOS_ENV)/clang-arm-fropi/bin/
|
||||
GCCPATH := $(BOLOS_ENV)/gcc-arm-none-eabi-5_3-2016q1/bin/
|
||||
$(info BOLOS_ENV=$(BOLOS_ENV))
|
||||
CLANGPATH := $(BOLOS_ENV)/clang-arm-fropi/bin/
|
||||
GCCPATH := $(BOLOS_ENV)/gcc-arm-none-eabi-5_3-2016q1/bin/
|
||||
else
|
||||
$(info BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH)
|
||||
$(info BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH)
|
||||
endif
|
||||
ifeq ($(CLANGPATH),)
|
||||
$(info CLANGPATH is not set: clang will be used from PATH)
|
||||
$(info CLANGPATH is not set: clang will be used from PATH)
|
||||
endif
|
||||
ifeq ($(GCCPATH),)
|
||||
$(info GCCPATH is not set: arm-none-eabi-* will be used from PATH)
|
||||
$(info GCCPATH is not set: arm-none-eabi-* will be used from PATH)
|
||||
endif
|
||||
|
||||
CC := $(CLANGPATH)clang
|
||||
|
||||
CFLAGS += -Wno-format-invalid-specifier -Wno-format-extra-args
|
||||
|
||||
AS := $(GCCPATH)arm-none-eabi-gcc
|
||||
|
||||
LD := $(GCCPATH)arm-none-eabi-gcc
|
||||
LDLIBS += -lm -lgcc -lc
|
||||
CC := $(CLANGPATH)clang
|
||||
CFLAGS += -Wno-format-invalid-specifier -Wno-format-extra-args
|
||||
AS := $(GCCPATH)arm-none-eabi-gcc
|
||||
LD := $(GCCPATH)arm-none-eabi-gcc
|
||||
LDLIBS += -lm -lgcc -lc
|
||||
|
||||
# import rules to compile glyphs(/pone)
|
||||
include $(BOLOS_SDK)/Makefile.glyphs
|
||||
|
||||
### variables processed by the common makefile.rules of the SDK to grab source files and include dirs
|
||||
APP_SOURCE_PATH += src_common src src_features src_plugins
|
||||
SDK_SOURCE_PATH += lib_stusb lib_stusb_impl lib_u2f
|
||||
APP_SOURCE_PATH += src_common src src_features src_plugins
|
||||
SDK_SOURCE_PATH += lib_stusb lib_stusb_impl lib_u2f
|
||||
ifeq ($(TARGET_NAME),TARGET_STAX)
|
||||
APP_SOURCE_PATH += src_nbgl
|
||||
APP_SOURCE_PATH += src_nbgl
|
||||
else
|
||||
SDK_SOURCE_PATH += lib_ux
|
||||
APP_SOURCE_PATH += src_bagl
|
||||
SDK_SOURCE_PATH += lib_ux
|
||||
APP_SOURCE_PATH += src_bagl
|
||||
endif
|
||||
|
||||
# Allow usage of function from lib_standard_app/crypto_helpers.c
|
||||
APP_SOURCE_FILES += ${BOLOS_SDK}/lib_standard_app/crypto_helpers.c
|
||||
|
||||
### initialize plugin SDK submodule if needed, rebuild it, and warn if a difference is noticed
|
||||
ifeq ($(CHAIN),ethereum)
|
||||
ifneq ($(shell git submodule status | grep '^[-+]'),)
|
||||
$(info INFO: Need to reinitialize git submodules)
|
||||
$(shell git submodule update --init)
|
||||
endif
|
||||
ifneq ($(shell git submodule status | grep '^[-+]'),)
|
||||
$(info INFO: Need to reinitialize git submodules)
|
||||
$(shell git submodule update --init)
|
||||
endif
|
||||
|
||||
# rebuild SDK
|
||||
$(shell python3 tools/build_sdk.py)
|
||||
# rebuild SDK
|
||||
$(shell ./tools/build_sdk.sh)
|
||||
|
||||
# check if a difference is noticed (fail if it happens in CI build)
|
||||
ifneq ($(shell git status | grep 'ethereum-plugin-sdk'),)
|
||||
ifneq ($(JENKINS_URL),)
|
||||
$(error ERROR: please update ethereum-plugin-sdk submodule first)
|
||||
else
|
||||
$(warning WARNING: please update ethereum-plugin-sdk submodule first)
|
||||
endif
|
||||
endif
|
||||
# check if a difference is noticed (fail if it happens in CI build)
|
||||
ifneq ($(shell git status | grep 'ethereum-plugin-sdk'),)
|
||||
ifneq ($(JENKINS_URL),)
|
||||
$(error ERROR: please update ethereum-plugin-sdk submodule first)
|
||||
else
|
||||
$(warning WARNING: please update ethereum-plugin-sdk submodule first)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
load: all
|
||||
@@ -279,6 +306,16 @@ test: install_tests run_tests
|
||||
unit-test:
|
||||
make -C tests/unit
|
||||
|
||||
ifeq ($(TARGET_NAME),TARGET_STAX)
|
||||
NETWORK_ICONS_FILE = $(GEN_SRC_DIR)/net_icons.gen.c
|
||||
NETWORK_ICONS_DIR = $(shell dirname "$(NETWORK_ICONS_FILE)")
|
||||
|
||||
$(NETWORK_ICONS_FILE):
|
||||
$(shell python3 tools/gen_networks.py "$(NETWORK_ICONS_DIR)")
|
||||
|
||||
APP_SOURCE_FILES += $(NETWORK_ICONS_FILE)
|
||||
endif
|
||||
|
||||
# import generic rules from the sdk
|
||||
include $(BOLOS_SDK)/Makefile.rules
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ Ledger Blue is not maintained anymore, but the app can still be compiled for thi
|
||||
|
||||
This app follows the specification available in the `doc/` folder.
|
||||
|
||||
To compile it and load it on a device, please check out our [developer portal](https://developers.ledger.com/docs/nano-app/introduction/).
|
||||
To compile it and load it on a device, please check out our [developer portal](https://developers.ledger.com/docs/device-app/introduction).
|
||||
|
||||
### Plugins
|
||||
|
||||
|
||||
4
client/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*egg-info
|
||||
dist
|
||||
*wheel
|
||||
*~
|
||||
53
client/CHANGELOG.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.0] - 2024-02-13
|
||||
|
||||
### Added
|
||||
|
||||
- New `provide_token_metadata` function
|
||||
|
||||
### Fixed
|
||||
|
||||
- Increased the delay between `autonext` callback calls for EIP-712 on Stax
|
||||
- `recover_transaction` util function for non-legacy transactions
|
||||
|
||||
## [0.2.1] - 2023-12-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- v0.2.0 version already published on pypi.org
|
||||
|
||||
## [0.2.0] - 2023-12-01
|
||||
|
||||
### Added
|
||||
|
||||
- New generic `sign` function that uses the Web3.py library
|
||||
|
||||
### Removed
|
||||
|
||||
- `sign_legacy` & `sign_1559` functions
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now uses the proper signing key for the `SET_EXTERNAL_PLUGIN` APDU
|
||||
|
||||
### Changed
|
||||
|
||||
- `get_public_addr` now returns address as `bytes` instead of `str`
|
||||
|
||||
## [0.1.0] - 2023-10-30
|
||||
|
||||
### Added
|
||||
|
||||
- Update the ragger app client to support "set external plugin" APDU
|
||||
|
||||
## [0.0.1] - 2023-07-08
|
||||
|
||||
### Added
|
||||
|
||||
- Initial version
|
||||
1
client/MANIFEST.in
Normal file
@@ -0,0 +1 @@
|
||||
include src/ledger_app_clients/ethereum/keychain/*
|
||||
20
client/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Python client for the Ledger Ethereum application
|
||||
|
||||
This package allows to communicate with the Ledger Ethereum application, either on a
|
||||
real device, or emulated on Speculos.
|
||||
|
||||
## Installation from pypi.org
|
||||
|
||||
You can install the client from `pypi.org`:
|
||||
|
||||
```bash
|
||||
pip install ledger_app_clients.ethereum
|
||||
```
|
||||
|
||||
## Installation from sources
|
||||
|
||||
You can also install the client from this repo:
|
||||
|
||||
```bash
|
||||
pip install .
|
||||
```
|
||||
51
client/pyproject.toml
Normal file
@@ -0,0 +1,51 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=45",
|
||||
"setuptools_scm[toml]>=6.2",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "ledger_app_clients.ethereum"
|
||||
authors = [
|
||||
{ name = "Ledger", email = "hello@ledger.fr" }
|
||||
]
|
||||
description = "Ledger Ethereum Python client"
|
||||
readme = { file = "README.md", content-type = "text/markdown" }
|
||||
# license = { file = "LICENSE" }
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
]
|
||||
dynamic = [ "version" ]
|
||||
requires-python = ">=3.7"
|
||||
dependencies = [
|
||||
"ragger[speculos]",
|
||||
"web3~=6.0",
|
||||
]
|
||||
|
||||
[tools.setuptools]
|
||||
include-package-data = true
|
||||
|
||||
[tool.setuptools_scm]
|
||||
version_file = "src/ledger_app_clients/ethereum/__version__.py"
|
||||
local_scheme = "no-local-version"
|
||||
root = "../"
|
||||
git_describe_command = "git describe --dirty --tags --long --match client-*[0-9]*"
|
||||
fallback_version = "0.0.0"
|
||||
|
||||
[project.urls]
|
||||
Home = "https://github.com/LedgerHQ/app-ethereum"
|
||||
|
||||
[tool.mypy]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.flake8]
|
||||
max-line-length = 120
|
||||
4
client/src/ledger_app_clients/ethereum/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
from ledger_app_clients.ethereum.__version__ import __version__ # noqa
|
||||
except ImportError:
|
||||
__version__ = "unknown version" # noqa
|
||||
255
client/src/ledger_app_clients/ethereum/client.py
Normal file
@@ -0,0 +1,255 @@
|
||||
import rlp
|
||||
from enum import IntEnum
|
||||
from ragger.backend import BackendInterface
|
||||
from ragger.utils import RAPDU
|
||||
from typing import Optional
|
||||
|
||||
from .command_builder import CommandBuilder
|
||||
from .eip712 import EIP712FieldType
|
||||
from .keychain import sign_data, Key
|
||||
from .tlv import format_tlv
|
||||
|
||||
from web3 import Web3
|
||||
|
||||
|
||||
class StatusWord(IntEnum):
|
||||
OK = 0x9000
|
||||
ERROR_NO_INFO = 0x6a00
|
||||
INVALID_DATA = 0x6a80
|
||||
INSUFFICIENT_MEMORY = 0x6a84
|
||||
INVALID_INS = 0x6d00
|
||||
INVALID_P1_P2 = 0x6b00
|
||||
CONDITION_NOT_SATISFIED = 0x6985
|
||||
REF_DATA_NOT_FOUND = 0x6a88
|
||||
|
||||
|
||||
class DomainNameTag(IntEnum):
|
||||
STRUCTURE_TYPE = 0x01
|
||||
STRUCTURE_VERSION = 0x02
|
||||
CHALLENGE = 0x12
|
||||
SIGNER_KEY_ID = 0x13
|
||||
SIGNER_ALGO = 0x14
|
||||
SIGNATURE = 0x15
|
||||
DOMAIN_NAME = 0x20
|
||||
COIN_TYPE = 0x21
|
||||
ADDRESS = 0x22
|
||||
|
||||
|
||||
class EthAppClient:
|
||||
def __init__(self, client: BackendInterface):
|
||||
self._client = client
|
||||
self._cmd_builder = CommandBuilder()
|
||||
|
||||
def _send(self, payload: bytes):
|
||||
return self._client.exchange_async_raw(payload)
|
||||
|
||||
def response(self) -> Optional[RAPDU]:
|
||||
return self._client.last_async_response
|
||||
|
||||
def eip712_send_struct_def_struct_name(self, name: str):
|
||||
return self._send(self._cmd_builder.eip712_send_struct_def_struct_name(name))
|
||||
|
||||
def eip712_send_struct_def_struct_field(self,
|
||||
field_type: EIP712FieldType,
|
||||
type_name: str,
|
||||
type_size: int,
|
||||
array_levels: list,
|
||||
key_name: str):
|
||||
return self._send(self._cmd_builder.eip712_send_struct_def_struct_field(
|
||||
field_type,
|
||||
type_name,
|
||||
type_size,
|
||||
array_levels,
|
||||
key_name))
|
||||
|
||||
def eip712_send_struct_impl_root_struct(self, name: str):
|
||||
return self._send(self._cmd_builder.eip712_send_struct_impl_root_struct(name))
|
||||
|
||||
def eip712_send_struct_impl_array(self, size: int):
|
||||
return self._send(self._cmd_builder.eip712_send_struct_impl_array(size))
|
||||
|
||||
def eip712_send_struct_impl_struct_field(self, raw_value: bytes):
|
||||
chunks = self._cmd_builder.eip712_send_struct_impl_struct_field(bytearray(raw_value))
|
||||
for chunk in chunks[:-1]:
|
||||
with self._send(chunk):
|
||||
pass
|
||||
return self._send(chunks[-1])
|
||||
|
||||
def eip712_sign_new(self, bip32_path: str):
|
||||
return self._send(self._cmd_builder.eip712_sign_new(bip32_path))
|
||||
|
||||
def eip712_sign_legacy(self,
|
||||
bip32_path: str,
|
||||
domain_hash: bytes,
|
||||
message_hash: bytes):
|
||||
return self._send(self._cmd_builder.eip712_sign_legacy(bip32_path,
|
||||
domain_hash,
|
||||
message_hash))
|
||||
|
||||
def eip712_filtering_activate(self):
|
||||
return self._send(self._cmd_builder.eip712_filtering_activate())
|
||||
|
||||
def eip712_filtering_message_info(self, name: str, filters_count: int, sig: bytes):
|
||||
return self._send(self._cmd_builder.eip712_filtering_message_info(name, filters_count, sig))
|
||||
|
||||
def eip712_filtering_show_field(self, name: str, sig: bytes):
|
||||
return self._send(self._cmd_builder.eip712_filtering_show_field(name, sig))
|
||||
|
||||
def sign(self,
|
||||
bip32_path: str,
|
||||
tx_params: dict):
|
||||
tx = Web3().eth.account.create().sign_transaction(tx_params).rawTransaction
|
||||
prefix = bytes()
|
||||
suffix = []
|
||||
if tx[0] in [0x01, 0x02]:
|
||||
prefix = tx[:1]
|
||||
tx = tx[len(prefix):]
|
||||
else: # legacy
|
||||
if "chainId" in tx_params:
|
||||
suffix = [int(tx_params["chainId"]), bytes(), bytes()]
|
||||
decoded = rlp.decode(tx)[:-3] # remove already computed signature
|
||||
tx = prefix + rlp.encode(decoded + suffix)
|
||||
chunks = self._cmd_builder.sign(bip32_path, tx, suffix)
|
||||
for chunk in chunks[:-1]:
|
||||
with self._send(chunk):
|
||||
pass
|
||||
return self._send(chunks[-1])
|
||||
|
||||
def get_challenge(self):
|
||||
return self._send(self._cmd_builder.get_challenge())
|
||||
|
||||
def get_public_addr(self,
|
||||
display: bool = True,
|
||||
chaincode: bool = False,
|
||||
bip32_path: str = "m/44'/60'/0'/0/0",
|
||||
chain_id: Optional[int] = None):
|
||||
return self._send(self._cmd_builder.get_public_addr(display,
|
||||
chaincode,
|
||||
bip32_path,
|
||||
chain_id))
|
||||
|
||||
def provide_domain_name(self, challenge: int, name: str, addr: bytes):
|
||||
payload = format_tlv(DomainNameTag.STRUCTURE_TYPE, 3) # TrustedDomainName
|
||||
payload += format_tlv(DomainNameTag.STRUCTURE_VERSION, 1)
|
||||
payload += format_tlv(DomainNameTag.SIGNER_KEY_ID, 0) # test key
|
||||
payload += format_tlv(DomainNameTag.SIGNER_ALGO, 1) # secp256k1
|
||||
payload += format_tlv(DomainNameTag.CHALLENGE, challenge)
|
||||
payload += format_tlv(DomainNameTag.COIN_TYPE, 0x3c) # ETH in slip-44
|
||||
payload += format_tlv(DomainNameTag.DOMAIN_NAME, name)
|
||||
payload += format_tlv(DomainNameTag.ADDRESS, addr)
|
||||
payload += format_tlv(DomainNameTag.SIGNATURE,
|
||||
sign_data(Key.DOMAIN_NAME, payload))
|
||||
|
||||
chunks = self._cmd_builder.provide_domain_name(payload)
|
||||
for chunk in chunks[:-1]:
|
||||
with self._send(chunk):
|
||||
pass
|
||||
return self._send(chunks[-1])
|
||||
|
||||
def set_plugin(self,
|
||||
plugin_name: str,
|
||||
contract_addr: bytes,
|
||||
selector: bytes,
|
||||
chain_id: int,
|
||||
type_: int = 1,
|
||||
version: int = 1,
|
||||
key_id: int = 2,
|
||||
algo_id: int = 1,
|
||||
sig: Optional[bytes] = None):
|
||||
if sig is None:
|
||||
# Temporarily get a command with an empty signature to extract the payload and
|
||||
# compute the signature on it
|
||||
tmp = self._cmd_builder.set_plugin(type_,
|
||||
version,
|
||||
plugin_name,
|
||||
contract_addr,
|
||||
selector,
|
||||
chain_id,
|
||||
key_id,
|
||||
algo_id,
|
||||
bytes())
|
||||
# skip APDU header & empty sig
|
||||
sig = sign_data(Key.SET_PLUGIN, tmp[5:-1])
|
||||
return self._send(self._cmd_builder.set_plugin(type_,
|
||||
version,
|
||||
plugin_name,
|
||||
contract_addr,
|
||||
selector,
|
||||
chain_id,
|
||||
key_id,
|
||||
algo_id,
|
||||
sig))
|
||||
|
||||
def provide_nft_metadata(self,
|
||||
collection: str,
|
||||
addr: bytes,
|
||||
chain_id: int,
|
||||
type_: int = 1,
|
||||
version: int = 1,
|
||||
key_id: int = 1,
|
||||
algo_id: int = 1,
|
||||
sig: Optional[bytes] = None):
|
||||
if sig is None:
|
||||
# Temporarily get a command with an empty signature to extract the payload and
|
||||
# compute the signature on it
|
||||
tmp = self._cmd_builder.provide_nft_information(type_,
|
||||
version,
|
||||
collection,
|
||||
addr,
|
||||
chain_id,
|
||||
key_id,
|
||||
algo_id,
|
||||
bytes())
|
||||
# skip APDU header & empty sig
|
||||
sig = sign_data(Key.NFT, tmp[5:-1])
|
||||
return self._send(self._cmd_builder.provide_nft_information(type_,
|
||||
version,
|
||||
collection,
|
||||
addr,
|
||||
chain_id,
|
||||
key_id,
|
||||
algo_id,
|
||||
sig))
|
||||
|
||||
def set_external_plugin(self,
|
||||
plugin_name: str,
|
||||
contract_address: bytes,
|
||||
method_selelector: bytes,
|
||||
sig: Optional[bytes] = None):
|
||||
if sig is None:
|
||||
# Temporarily get a command with an empty signature to extract the payload and
|
||||
# compute the signature on it
|
||||
tmp = self._cmd_builder.set_external_plugin(plugin_name, contract_address, method_selelector, bytes())
|
||||
|
||||
# skip APDU header & empty sig
|
||||
sig = sign_data(Key.CAL, tmp[5:])
|
||||
return self._send(self._cmd_builder.set_external_plugin(plugin_name, contract_address, method_selelector, sig))
|
||||
|
||||
def personal_sign(self, path: str, msg: bytes):
|
||||
chunks = self._cmd_builder.personal_sign(path, msg)
|
||||
for chunk in chunks[:-1]:
|
||||
with self._send(chunk):
|
||||
pass
|
||||
return self._send(chunks[-1])
|
||||
|
||||
def provide_token_metadata(self,
|
||||
ticker: str,
|
||||
addr: bytes,
|
||||
decimals: int,
|
||||
chain_id: int,
|
||||
sig: Optional[bytes] = None):
|
||||
if sig is None:
|
||||
# Temporarily get a command with an empty signature to extract the payload and
|
||||
# compute the signature on it
|
||||
tmp = self._cmd_builder.provide_erc20_token_information(ticker,
|
||||
addr,
|
||||
decimals,
|
||||
chain_id,
|
||||
bytes())
|
||||
# skip APDU header & empty sig
|
||||
sig = sign_data(Key.CAL, tmp[6:])
|
||||
return self._send(self._cmd_builder.provide_erc20_token_information(ticker,
|
||||
addr,
|
||||
decimals,
|
||||
chain_id,
|
||||
sig))
|
||||
@@ -1,17 +1,29 @@
|
||||
from enum import IntEnum, auto
|
||||
from typing import Iterator, Optional
|
||||
from .eip712 import EIP712FieldType
|
||||
from ragger.bip import pack_derivation_path
|
||||
# documentation about APDU format is available here:
|
||||
# https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc
|
||||
|
||||
import struct
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
from ragger.bip import pack_derivation_path
|
||||
|
||||
from .eip712 import EIP712FieldType
|
||||
|
||||
|
||||
class InsType(IntEnum):
|
||||
GET_PUBLIC_ADDR = 0x02
|
||||
SIGN = 0x04
|
||||
PERSONAL_SIGN = 0x08
|
||||
PROVIDE_ERC20_TOKEN_INFORMATION = 0x0a
|
||||
PROVIDE_NFT_INFORMATION = 0x14
|
||||
SET_PLUGIN = 0x16
|
||||
EIP712_SEND_STRUCT_DEF = 0x1a
|
||||
EIP712_SEND_STRUCT_IMPL = 0x1c
|
||||
EIP712_SEND_FILTERING = 0x1e
|
||||
EIP712_SIGN = 0x0c
|
||||
GET_CHALLENGE = 0x20
|
||||
PROVIDE_DOMAIN_NAME = 0x22
|
||||
EXTERNAL_PLUGIN_SETUP = 0x12
|
||||
|
||||
|
||||
class P1Type(IntEnum):
|
||||
COMPLETE_SEND = 0x00
|
||||
@@ -19,6 +31,7 @@ class P1Type(IntEnum):
|
||||
SIGN_FIRST_CHUNK = 0x00
|
||||
SIGN_SUBSQT_CHUNK = 0x80
|
||||
|
||||
|
||||
class P2Type(IntEnum):
|
||||
STRUCT_NAME = 0x00
|
||||
STRUCT_FIELD = 0xff
|
||||
@@ -29,6 +42,7 @@ class P2Type(IntEnum):
|
||||
FILTERING_CONTRACT_NAME = 0x0f
|
||||
FILTERING_FIELD_NAME = 0xff
|
||||
|
||||
|
||||
class CommandBuilder:
|
||||
_CLA: int = 0xE0
|
||||
|
||||
@@ -36,7 +50,7 @@ class CommandBuilder:
|
||||
ins: InsType,
|
||||
p1: int,
|
||||
p2: int,
|
||||
cdata: bytearray = bytes()) -> bytes:
|
||||
cdata: bytes = bytes()) -> bytes:
|
||||
|
||||
header = bytearray()
|
||||
header.append(self._CLA)
|
||||
@@ -62,24 +76,24 @@ class CommandBuilder:
|
||||
field_type: EIP712FieldType,
|
||||
type_name: str,
|
||||
type_size: int,
|
||||
array_levels: [],
|
||||
array_levels: list,
|
||||
key_name: str) -> bytes:
|
||||
data = bytearray()
|
||||
typedesc = 0
|
||||
typedesc |= (len(array_levels) > 0) << 7
|
||||
typedesc |= (type_size != None) << 6
|
||||
typedesc |= (type_size is not None) << 6
|
||||
typedesc |= field_type
|
||||
data.append(typedesc)
|
||||
if field_type == EIP712FieldType.CUSTOM:
|
||||
data.append(len(type_name))
|
||||
data += self._string_to_bytes(type_name)
|
||||
if type_size != None:
|
||||
if type_size is not None:
|
||||
data.append(type_size)
|
||||
if len(array_levels) > 0:
|
||||
data.append(len(array_levels))
|
||||
for level in array_levels:
|
||||
data.append(0 if level == None else 1)
|
||||
if level != None:
|
||||
data.append(0 if level is None else 1)
|
||||
if level is not None:
|
||||
data.append(level)
|
||||
data.append(len(key_name))
|
||||
data += self._string_to_bytes(key_name)
|
||||
@@ -102,7 +116,7 @@ class CommandBuilder:
|
||||
P2Type.ARRAY,
|
||||
data)
|
||||
|
||||
def eip712_send_struct_impl_struct_field(self, data: bytearray) -> Iterator[bytes]:
|
||||
def eip712_send_struct_impl_struct_field(self, data: bytearray) -> list[bytes]:
|
||||
chunks = list()
|
||||
# Add a 16-bit integer with the data's byte length (network byte order)
|
||||
data_w_length = bytearray()
|
||||
@@ -169,17 +183,40 @@ class CommandBuilder:
|
||||
P2Type.FILTERING_FIELD_NAME,
|
||||
self._eip712_filtering_send_name(name, sig))
|
||||
|
||||
def sign(self, bip32_path: str, rlp_data: bytes) -> list[bytes]:
|
||||
def set_external_plugin(self, plugin_name: str, contract_address: bytes, selector: bytes, sig: bytes) -> bytes:
|
||||
data = bytearray()
|
||||
data.append(len(plugin_name))
|
||||
data += self._string_to_bytes(plugin_name)
|
||||
data += contract_address
|
||||
data += selector
|
||||
data += sig
|
||||
|
||||
return self._serialize(InsType.EXTERNAL_PLUGIN_SETUP,
|
||||
P1Type.COMPLETE_SEND,
|
||||
0x00,
|
||||
data)
|
||||
|
||||
def sign(self, bip32_path: str, rlp_data: bytes, vrs: list) -> list[bytes]:
|
||||
apdus = list()
|
||||
payload = pack_derivation_path(bip32_path)
|
||||
payload += rlp_data
|
||||
p1 = P1Type.SIGN_FIRST_CHUNK
|
||||
while len(payload) > 0:
|
||||
chunk_size = 0xff
|
||||
|
||||
# TODO: Fix the app & remove this, issue #409
|
||||
if len(vrs) == 3:
|
||||
if len(payload) > chunk_size:
|
||||
import rlp
|
||||
diff = len(rlp.encode(vrs)) - (len(payload) - chunk_size)
|
||||
if diff > 0:
|
||||
chunk_size -= diff
|
||||
|
||||
apdus.append(self._serialize(InsType.SIGN,
|
||||
p1,
|
||||
0x00,
|
||||
payload[:0xff]))
|
||||
payload = payload[0xff:]
|
||||
payload[:chunk_size]))
|
||||
payload = payload[chunk_size:]
|
||||
p1 = P1Type.SIGN_SUBSQT_CHUNK
|
||||
return apdus
|
||||
|
||||
@@ -188,7 +225,7 @@ class CommandBuilder:
|
||||
|
||||
def provide_domain_name(self, tlv_payload: bytes) -> list[bytes]:
|
||||
chunks = list()
|
||||
payload = struct.pack(">H", len(tlv_payload))
|
||||
payload = struct.pack(">H", len(tlv_payload))
|
||||
payload += tlv_payload
|
||||
p1 = 1
|
||||
while len(payload) > 0:
|
||||
@@ -199,3 +236,96 @@ class CommandBuilder:
|
||||
payload = payload[0xff:]
|
||||
p1 = 0
|
||||
return chunks
|
||||
|
||||
def get_public_addr(self,
|
||||
display: bool,
|
||||
chaincode: bool,
|
||||
bip32_path: str,
|
||||
chain_id: Optional[int]) -> bytes:
|
||||
payload = pack_derivation_path(bip32_path)
|
||||
if chain_id is not None:
|
||||
payload += struct.pack(">Q", chain_id)
|
||||
return self._serialize(InsType.GET_PUBLIC_ADDR,
|
||||
int(display),
|
||||
int(chaincode),
|
||||
payload)
|
||||
|
||||
def set_plugin(self,
|
||||
type_: int,
|
||||
version: int,
|
||||
plugin_name: str,
|
||||
contract_addr: bytes,
|
||||
selector: bytes,
|
||||
chain_id: int,
|
||||
key_id: int,
|
||||
algo_id: int,
|
||||
sig: bytes) -> bytes:
|
||||
payload = bytearray()
|
||||
payload.append(type_)
|
||||
payload.append(version)
|
||||
payload.append(len(plugin_name))
|
||||
payload += plugin_name.encode()
|
||||
payload += contract_addr
|
||||
payload += selector
|
||||
payload += struct.pack(">Q", chain_id)
|
||||
payload.append(key_id)
|
||||
payload.append(algo_id)
|
||||
payload.append(len(sig))
|
||||
payload += sig
|
||||
return self._serialize(InsType.SET_PLUGIN, 0x00, 0x00, payload)
|
||||
|
||||
def provide_nft_information(self,
|
||||
type_: int,
|
||||
version: int,
|
||||
collection_name: str,
|
||||
addr: bytes,
|
||||
chain_id: int,
|
||||
key_id: int,
|
||||
algo_id: int,
|
||||
sig: bytes):
|
||||
payload = bytearray()
|
||||
payload.append(type_)
|
||||
payload.append(version)
|
||||
payload.append(len(collection_name))
|
||||
payload += collection_name.encode()
|
||||
payload += addr
|
||||
payload += struct.pack(">Q", chain_id)
|
||||
payload.append(key_id)
|
||||
payload.append(algo_id)
|
||||
payload.append(len(sig))
|
||||
payload += sig
|
||||
return self._serialize(InsType.PROVIDE_NFT_INFORMATION, 0x00, 0x00, payload)
|
||||
|
||||
def personal_sign(self, path: str, msg: bytes):
|
||||
payload = pack_derivation_path(path)
|
||||
payload += struct.pack(">I", len(msg))
|
||||
payload += msg
|
||||
chunks = list()
|
||||
p1 = P1Type.SIGN_FIRST_CHUNK
|
||||
while len(payload) > 0:
|
||||
chunk_size = 0xff
|
||||
chunks.append(self._serialize(InsType.PERSONAL_SIGN,
|
||||
p1,
|
||||
0x00,
|
||||
payload[:chunk_size]))
|
||||
payload = payload[chunk_size:]
|
||||
p1 = P1Type.SIGN_SUBSQT_CHUNK
|
||||
return chunks
|
||||
|
||||
def provide_erc20_token_information(self,
|
||||
ticker: str,
|
||||
addr: bytes,
|
||||
decimals: int,
|
||||
chain_id: int,
|
||||
sig: bytes) -> bytes:
|
||||
payload = bytearray()
|
||||
payload.append(len(ticker))
|
||||
payload += ticker.encode()
|
||||
payload += addr
|
||||
payload += struct.pack(">I", decimals)
|
||||
payload += struct.pack(">I", chain_id)
|
||||
payload += sig
|
||||
return self._serialize(InsType.PROVIDE_ERC20_TOKEN_INFORMATION,
|
||||
0x00,
|
||||
0x00,
|
||||
payload)
|
||||
@@ -1,23 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import hashlib
|
||||
from app.client import EthAppClient, EIP712FieldType
|
||||
import keychain
|
||||
from typing import Callable
|
||||
import json
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
import copy
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from ledger_app_clients.ethereum import keychain
|
||||
from ledger_app_clients.ethereum.client import EthAppClient, EIP712FieldType
|
||||
|
||||
|
||||
# global variables
|
||||
app_client: EthAppClient = None
|
||||
filtering_paths = None
|
||||
current_path = list()
|
||||
sig_ctx = {}
|
||||
|
||||
autonext_handler: Callable = None
|
||||
filtering_paths: dict = {}
|
||||
current_path: list[str] = list()
|
||||
sig_ctx: dict[str, Any] = {}
|
||||
|
||||
|
||||
def default_handler():
|
||||
raise RuntimeError("Uninitialized handler")
|
||||
|
||||
|
||||
autonext_handler: Callable = default_handler
|
||||
|
||||
|
||||
# From a string typename, extract the type and all the array depth
|
||||
@@ -57,29 +61,34 @@ def get_typesize(typename):
|
||||
return (typename, typesize)
|
||||
|
||||
|
||||
|
||||
def parse_int(typesize):
|
||||
return (EIP712FieldType.INT, int(typesize / 8))
|
||||
|
||||
|
||||
def parse_uint(typesize):
|
||||
return (EIP712FieldType.UINT, int(typesize / 8))
|
||||
|
||||
|
||||
def parse_address(typesize):
|
||||
return (EIP712FieldType.ADDRESS, None)
|
||||
|
||||
|
||||
def parse_bool(typesize):
|
||||
return (EIP712FieldType.BOOL, None)
|
||||
|
||||
|
||||
def parse_string(typesize):
|
||||
return (EIP712FieldType.STRING, None)
|
||||
|
||||
|
||||
def parse_bytes(typesize):
|
||||
if typesize != None:
|
||||
if typesize is not None:
|
||||
return (EIP712FieldType.FIX_BYTES, typesize)
|
||||
return (EIP712FieldType.DYN_BYTES, None)
|
||||
|
||||
|
||||
# set functions for each type
|
||||
parsing_type_functions = {};
|
||||
parsing_type_functions = {}
|
||||
parsing_type_functions["int"] = parse_int
|
||||
parsing_type_functions["uint"] = parse_uint
|
||||
parsing_type_functions["address"] = parse_address
|
||||
@@ -88,7 +97,6 @@ parsing_type_functions["string"] = parse_string
|
||||
parsing_type_functions["bytes"] = parse_bytes
|
||||
|
||||
|
||||
|
||||
def send_struct_def_field(typename, keyname):
|
||||
type_enum = None
|
||||
|
||||
@@ -110,7 +118,6 @@ def send_struct_def_field(typename, keyname):
|
||||
return (typename, type_enum, typesize, array_lvls)
|
||||
|
||||
|
||||
|
||||
def encode_integer(value, typesize):
|
||||
data = bytearray()
|
||||
|
||||
@@ -124,9 +131,9 @@ def encode_integer(value, typesize):
|
||||
if value == 0:
|
||||
data.append(0)
|
||||
else:
|
||||
if value < 0: # negative number, send it as unsigned
|
||||
if value < 0: # negative number, send it as unsigned
|
||||
mask = 0
|
||||
for i in range(typesize): # make a mask as big as the typesize
|
||||
for i in range(typesize): # make a mask as big as the typesize
|
||||
mask = (mask << 8) | 0xff
|
||||
value &= mask
|
||||
while value > 0:
|
||||
@@ -135,42 +142,51 @@ def encode_integer(value, typesize):
|
||||
data.reverse()
|
||||
return data
|
||||
|
||||
|
||||
def encode_int(value, typesize):
|
||||
return encode_integer(value, typesize)
|
||||
|
||||
|
||||
def encode_uint(value, typesize):
|
||||
return encode_integer(value, typesize)
|
||||
|
||||
|
||||
def encode_hex_string(value, size):
|
||||
data = bytearray()
|
||||
value = value[2:] # skip 0x
|
||||
value = value[2:] # skip 0x
|
||||
byte_idx = 0
|
||||
while byte_idx < size:
|
||||
data.append(int(value[(byte_idx * 2):(byte_idx * 2 + 2)], 16))
|
||||
byte_idx += 1
|
||||
return data
|
||||
|
||||
|
||||
def encode_address(value, typesize):
|
||||
return encode_hex_string(value, 20)
|
||||
|
||||
|
||||
def encode_bool(value, typesize):
|
||||
return encode_integer(value, typesize)
|
||||
|
||||
|
||||
def encode_string(value, typesize):
|
||||
data = bytearray()
|
||||
for char in value:
|
||||
data.append(ord(char))
|
||||
return data
|
||||
|
||||
|
||||
def encode_bytes_fix(value, typesize):
|
||||
return encode_hex_string(value, typesize)
|
||||
|
||||
|
||||
def encode_bytes_dyn(value, typesize):
|
||||
# length of the value string
|
||||
# - the length of 0x (2)
|
||||
# / by the length of one byte in a hex string (2)
|
||||
return encode_hex_string(value, int((len(value) - 2) / 2))
|
||||
|
||||
|
||||
# set functions for each type
|
||||
encoding_functions = {}
|
||||
encoding_functions[EIP712FieldType.INT] = encode_int
|
||||
@@ -182,7 +198,6 @@ encoding_functions[EIP712FieldType.FIX_BYTES] = encode_bytes_fix
|
||||
encoding_functions[EIP712FieldType.DYN_BYTES] = encode_bytes_dyn
|
||||
|
||||
|
||||
|
||||
def send_struct_impl_field(value, field):
|
||||
# Something wrong happened if this triggers
|
||||
if isinstance(value, list) or (field["enum"] == EIP712FieldType.CUSTOM):
|
||||
@@ -190,7 +205,6 @@ def send_struct_impl_field(value, field):
|
||||
|
||||
data = encoding_functions[field["enum"]](value, field["typesize"])
|
||||
|
||||
|
||||
if filtering_paths:
|
||||
path = ".".join(current_path)
|
||||
if path in filtering_paths.keys():
|
||||
@@ -201,8 +215,7 @@ def send_struct_impl_field(value, field):
|
||||
disable_autonext()
|
||||
|
||||
|
||||
|
||||
def evaluate_field(structs, data, field, lvls_left, new_level = True):
|
||||
def evaluate_field(structs, data, field, lvls_left, new_level=True):
|
||||
array_lvls = field["array_lvls"]
|
||||
|
||||
if new_level:
|
||||
@@ -217,7 +230,7 @@ def evaluate_field(structs, data, field, lvls_left, new_level = True):
|
||||
return False
|
||||
current_path.pop()
|
||||
idx += 1
|
||||
if array_lvls[lvls_left - 1] != None:
|
||||
if array_lvls[lvls_left - 1] is not None:
|
||||
if array_lvls[lvls_left - 1] != idx:
|
||||
print("Mismatch in array size! Got %d, expected %d\n" %
|
||||
(idx, array_lvls[lvls_left - 1]),
|
||||
@@ -234,7 +247,6 @@ def evaluate_field(structs, data, field, lvls_left, new_level = True):
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def send_struct_impl(structs, data, structname):
|
||||
# Check if it is a struct we don't known
|
||||
if structname not in structs.keys():
|
||||
@@ -246,6 +258,7 @@ def send_struct_impl(structs, data, structname):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures
|
||||
def send_filtering_message_info(display_name: str, filters_count: int):
|
||||
global sig_ctx
|
||||
@@ -264,6 +277,7 @@ def send_filtering_message_info(display_name: str, filters_count: int):
|
||||
enable_autonext()
|
||||
disable_autonext()
|
||||
|
||||
|
||||
# ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures
|
||||
def send_filtering_show_field(display_name):
|
||||
global sig_ctx
|
||||
@@ -283,11 +297,6 @@ def send_filtering_show_field(display_name):
|
||||
with app_client.eip712_filtering_show_field(display_name, sig):
|
||||
pass
|
||||
|
||||
def read_filtering_file(domain, message, filtering_file_path):
|
||||
data_json = None
|
||||
with open(filtering_file_path) as data:
|
||||
data_json = json.load(data)
|
||||
return data_json
|
||||
|
||||
def prepare_filtering(filtr_data, message):
|
||||
global filtering_paths
|
||||
@@ -297,12 +306,14 @@ def prepare_filtering(filtr_data, message):
|
||||
else:
|
||||
filtering_paths = {}
|
||||
|
||||
|
||||
def handle_optional_domain_values(domain):
|
||||
if "chainId" not in domain.keys():
|
||||
domain["chainId"] = 0
|
||||
if "verifyingContract" not in domain.keys():
|
||||
domain["verifyingContract"] = "0x0000000000000000000000000000000000000000"
|
||||
|
||||
|
||||
def init_signature_context(types, domain):
|
||||
global sig_ctx
|
||||
|
||||
@@ -316,7 +327,7 @@ def init_signature_context(types, domain):
|
||||
for i in range(8):
|
||||
sig_ctx["chainid"].append(chainid & (0xff << (i * 8)))
|
||||
sig_ctx["chainid"].reverse()
|
||||
schema_str = json.dumps(types).replace(" ","")
|
||||
schema_str = json.dumps(types).replace(" ", "")
|
||||
schema_hash = hashlib.sha224(schema_str.encode())
|
||||
sig_ctx["schema_hash"] = bytearray.fromhex(schema_hash.hexdigest())
|
||||
|
||||
@@ -324,74 +335,74 @@ def init_signature_context(types, domain):
|
||||
def next_timeout(_signum: int, _frame):
|
||||
autonext_handler()
|
||||
|
||||
|
||||
def enable_autonext():
|
||||
seconds = 1/4
|
||||
if app_client._client.firmware.device == 'stax': # Stax Speculos is slow
|
||||
interval = seconds * 3
|
||||
if app_client._client.firmware.device == 'stax': # Stax Speculos is slow
|
||||
delay = 1.5
|
||||
else:
|
||||
interval = seconds
|
||||
signal.setitimer(signal.ITIMER_REAL, seconds, interval)
|
||||
delay = 1/4
|
||||
signal.setitimer(signal.ITIMER_REAL, delay, delay)
|
||||
|
||||
|
||||
def disable_autonext():
|
||||
signal.setitimer(signal.ITIMER_REAL, 0, 0)
|
||||
|
||||
|
||||
def process_file(aclient: EthAppClient,
|
||||
input_file_path: str,
|
||||
filtering_file_path = None,
|
||||
autonext: Callable = None) -> bool:
|
||||
def process_data(aclient: EthAppClient,
|
||||
data_json: dict,
|
||||
filters: Optional[dict] = None,
|
||||
autonext: Optional[Callable] = None) -> bool:
|
||||
global sig_ctx
|
||||
global app_client
|
||||
global autonext_handler
|
||||
|
||||
# deepcopy because this function modifies the dict
|
||||
data_json = copy.deepcopy(data_json)
|
||||
app_client = aclient
|
||||
with open(input_file_path, "r") as data:
|
||||
data_json = json.load(data)
|
||||
domain_typename = "EIP712Domain"
|
||||
message_typename = data_json["primaryType"]
|
||||
types = data_json["types"]
|
||||
domain = data_json["domain"]
|
||||
message = data_json["message"]
|
||||
domain_typename = "EIP712Domain"
|
||||
message_typename = data_json["primaryType"]
|
||||
types = data_json["types"]
|
||||
domain = data_json["domain"]
|
||||
message = data_json["message"]
|
||||
|
||||
if autonext:
|
||||
autonext_handler = autonext
|
||||
signal.signal(signal.SIGALRM, next_timeout)
|
||||
if autonext:
|
||||
autonext_handler = autonext
|
||||
signal.signal(signal.SIGALRM, next_timeout)
|
||||
|
||||
if filtering_file_path:
|
||||
init_signature_context(types, domain)
|
||||
filtr = read_filtering_file(domain, message, filtering_file_path)
|
||||
if filters:
|
||||
init_signature_context(types, domain)
|
||||
|
||||
# send types definition
|
||||
for key in types.keys():
|
||||
with app_client.eip712_send_struct_def_struct_name(key):
|
||||
pass
|
||||
for f in types[key]:
|
||||
(f["type"], f["enum"], f["typesize"], f["array_lvls"]) = \
|
||||
send_struct_def_field(f["type"], f["name"])
|
||||
# send types definition
|
||||
for key in types.keys():
|
||||
with app_client.eip712_send_struct_def_struct_name(key):
|
||||
pass
|
||||
for f in types[key]:
|
||||
(f["type"], f["enum"], f["typesize"], f["array_lvls"]) = \
|
||||
send_struct_def_field(f["type"], f["name"])
|
||||
|
||||
if filtering_file_path:
|
||||
with app_client.eip712_filtering_activate():
|
||||
pass
|
||||
prepare_filtering(filtr, message)
|
||||
if filters:
|
||||
with app_client.eip712_filtering_activate():
|
||||
pass
|
||||
prepare_filtering(filters, message)
|
||||
|
||||
# send domain implementation
|
||||
with app_client.eip712_send_struct_impl_root_struct(domain_typename):
|
||||
enable_autonext()
|
||||
disable_autonext()
|
||||
if not send_struct_impl(types, domain, domain_typename):
|
||||
return False
|
||||
# send domain implementation
|
||||
with app_client.eip712_send_struct_impl_root_struct(domain_typename):
|
||||
enable_autonext()
|
||||
disable_autonext()
|
||||
if not send_struct_impl(types, domain, domain_typename):
|
||||
return False
|
||||
|
||||
if filtering_file_path:
|
||||
if filtr and "name" in filtr:
|
||||
send_filtering_message_info(filtr["name"], len(filtering_paths))
|
||||
else:
|
||||
send_filtering_message_info(domain["name"], len(filtering_paths))
|
||||
if filters:
|
||||
if filters and "name" in filters:
|
||||
send_filtering_message_info(filters["name"], len(filtering_paths))
|
||||
else:
|
||||
send_filtering_message_info(domain["name"], len(filtering_paths))
|
||||
|
||||
# send message implementation
|
||||
with app_client.eip712_send_struct_impl_root_struct(message_typename):
|
||||
enable_autonext()
|
||||
disable_autonext()
|
||||
if not send_struct_impl(types, message, message_typename):
|
||||
return False
|
||||
# send message implementation
|
||||
with app_client.eip712_send_struct_impl_root_struct(message_typename):
|
||||
enable_autonext()
|
||||
disable_autonext()
|
||||
if not send_struct_impl(types, message, message_typename):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -0,0 +1 @@
|
||||
from .struct import EIP712FieldType # noqa
|
||||
@@ -1,5 +1,6 @@
|
||||
from enum import IntEnum, auto
|
||||
|
||||
|
||||
class EIP712FieldType(IntEnum):
|
||||
CUSTOM = 0,
|
||||
INT = auto()
|
||||
@@ -1,23 +1,29 @@
|
||||
import os
|
||||
import hashlib
|
||||
from ecdsa.util import sigencode_der
|
||||
from ecdsa import SigningKey
|
||||
from ecdsa.util import sigencode_der
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
# Private key PEM files have to be named the same (lowercase) as their corresponding enum entries
|
||||
# Example: for an entry in the Enum named DEV, its PEM file must be at keychain/dev.pem
|
||||
class Key(Enum):
|
||||
CAL = auto()
|
||||
DOMAIN_NAME = auto()
|
||||
SET_PLUGIN = auto()
|
||||
NFT = auto()
|
||||
|
||||
|
||||
_keys: dict[Key, SigningKey] = dict()
|
||||
|
||||
|
||||
# Open the corresponding PEM file and load its key in the global dict
|
||||
def _init_key(key: Key):
|
||||
global _keys
|
||||
with open("%s/keychain/%s.pem" % (os.path.dirname(__file__), key.name.lower())) as pem_file:
|
||||
_keys[key] = SigningKey.from_pem(pem_file.read(), hashlib.sha256)
|
||||
assert (key in _keys) and (_keys[key] != None)
|
||||
assert (key in _keys) and (_keys[key] is not None)
|
||||
|
||||
|
||||
# Generate a SECP256K1 signature of the given data with the given key
|
||||
def sign_data(key: Key, data: bytes) -> bytes:
|
||||
8
client/src/ledger_app_clients/ethereum/keychain/nft.pem
Normal file
@@ -0,0 +1,8 @@
|
||||
-----BEGIN EC PARAMETERS-----
|
||||
BgUrgQQACg==
|
||||
-----END EC PARAMETERS-----
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHQCAQEEIK69Gt4o0bzkOaEwUE5X2tI+Ks80FQi785Co+6woU9hioAcGBSuBBAAK
|
||||
oUQDQgAEPPtfsxkF9L052dU1pAwmqrUcXX0yGbKKyUK5gPsgbPswtRzC3iEZrAOO
|
||||
uw191lQXcCBKPO06eeKLMvu2cmRowA==
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN EC PARAMETERS-----
|
||||
BgUrgQQACg==
|
||||
-----END EC PARAMETERS-----
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHQCAQEEIBErwcYvqeKSOlmQ/j3xPkVcwFf+j1aiMsA+RabczvN7oAcGBSuBBAAK
|
||||
oUQDQgAEwFW8Ts8FXi2FCF01Eno95nBcf4hQVc1wceh2cb8ZH+M8yPAavC8ofIGa
|
||||
FIq+G1gd8bSUCvXU3DpOa2AZF3ErNw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
52
client/src/ledger_app_clients/ethereum/response_parser.py
Normal file
@@ -0,0 +1,52 @@
|
||||
def signature(data: bytes) -> tuple[bytes, bytes, bytes]:
|
||||
assert len(data) == (1 + 32 + 32)
|
||||
|
||||
v = data[0:1]
|
||||
data = data[1:]
|
||||
r = data[0:32]
|
||||
data = data[32:]
|
||||
s = data[0:32]
|
||||
|
||||
return v, r, s
|
||||
|
||||
|
||||
def challenge(data: bytes) -> int:
|
||||
assert len(data) == 4
|
||||
return int.from_bytes(data, "big")
|
||||
|
||||
|
||||
def pk_addr(data: bytes, has_chaincode: bool = False):
|
||||
idx = 0
|
||||
|
||||
if len(data) < (idx + 1):
|
||||
return None
|
||||
pk_len = data[idx]
|
||||
idx += 1
|
||||
|
||||
if len(data) < (idx + pk_len):
|
||||
return None
|
||||
pk = data[idx:idx + pk_len]
|
||||
idx += pk_len
|
||||
|
||||
if len(data) < (idx + 1):
|
||||
return None
|
||||
addr_len = data[idx]
|
||||
idx += 1
|
||||
|
||||
if len(data) < (idx + addr_len):
|
||||
return None
|
||||
addr = data[idx:idx + addr_len]
|
||||
idx += addr_len
|
||||
|
||||
if has_chaincode:
|
||||
if len(data) < (idx + 32):
|
||||
return None
|
||||
chaincode = data[idx:idx + 32]
|
||||
idx += 32
|
||||
else:
|
||||
chaincode = None
|
||||
|
||||
if idx != len(data):
|
||||
return None
|
||||
|
||||
return pk, bytes.fromhex(addr.decode()), chaincode
|
||||
@@ -1,7 +1,8 @@
|
||||
from enum import Enum, auto
|
||||
from typing import List
|
||||
from ragger.firmware import Firmware
|
||||
from ragger.navigator import Navigator, NavInsID, NavIns
|
||||
from typing import Union
|
||||
|
||||
|
||||
class SettingID(Enum):
|
||||
BLIND_SIGNING = auto()
|
||||
@@ -10,6 +11,7 @@ class SettingID(Enum):
|
||||
VERBOSE_EIP712 = auto()
|
||||
VERBOSE_ENS = auto()
|
||||
|
||||
|
||||
def get_device_settings(device: str) -> list[SettingID]:
|
||||
if device == "nanos":
|
||||
return [
|
||||
@@ -27,19 +29,22 @@ def get_device_settings(device: str) -> list[SettingID]:
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
settings_per_page = 3
|
||||
|
||||
def get_setting_position(device: str, setting: NavInsID) -> tuple[int, int]:
|
||||
screen_height = 672 # px
|
||||
header_height = 85 # px
|
||||
footer_height = 124 # px
|
||||
|
||||
def get_setting_position(device: str, setting: Union[NavInsID, SettingID]) -> tuple[int, int]:
|
||||
screen_height = 672 # px
|
||||
header_height = 85 # px
|
||||
footer_height = 124 # px
|
||||
usable_height = screen_height - (header_height + footer_height)
|
||||
setting_height = usable_height // settings_per_page
|
||||
index_in_page = get_device_settings(device).index(setting) % settings_per_page
|
||||
index_in_page = get_device_settings(device).index(SettingID(setting)) % settings_per_page
|
||||
return 350, header_height + (setting_height * index_in_page) + (setting_height // 2)
|
||||
|
||||
|
||||
def settings_toggle(fw: Firmware, nav: Navigator, to_toggle: list[SettingID]):
|
||||
moves = list()
|
||||
moves: list[Union[NavIns, NavInsID]] = list()
|
||||
settings = get_device_settings(fw.device)
|
||||
# Assume the app is on the home page
|
||||
if fw.device.startswith("nano"):
|
||||
@@ -49,7 +54,7 @@ def settings_toggle(fw: Firmware, nav: Navigator, to_toggle: list[SettingID]):
|
||||
if setting in to_toggle:
|
||||
moves += [NavInsID.BOTH_CLICK]
|
||||
moves += [NavInsID.RIGHT_CLICK]
|
||||
moves += [NavInsID.BOTH_CLICK] # Back
|
||||
moves += [NavInsID.BOTH_CLICK] # Back
|
||||
else:
|
||||
moves += [NavInsID.USE_CASE_HOME_SETTINGS]
|
||||
moves += [NavInsID.USE_CASE_SETTINGS_NEXT]
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any
|
||||
from typing import Union
|
||||
|
||||
|
||||
def der_encode(value: int) -> bytes:
|
||||
# max() to have minimum length of 1
|
||||
@@ -7,16 +8,15 @@ def der_encode(value: int) -> bytes:
|
||||
value_bytes = (0x80 | len(value_bytes)).to_bytes(1, 'big') + value_bytes
|
||||
return value_bytes
|
||||
|
||||
def format_tlv(tag: int, value: Any) -> bytes:
|
||||
|
||||
def format_tlv(tag: int, value: Union[int, str, bytes]) -> bytes:
|
||||
if isinstance(value, int):
|
||||
# max() to have minimum length of 1
|
||||
value = value.to_bytes(max(1, (value.bit_length() + 7) // 8), 'big')
|
||||
elif isinstance(value, str):
|
||||
value = value.encode()
|
||||
|
||||
if not isinstance(value, bytes):
|
||||
print("Unhandled TLV formatting for type : %s" % (type(value)))
|
||||
return None
|
||||
assert isinstance(value, bytes), f"Unhandled TLV formatting for type : {type(value)}"
|
||||
|
||||
tlv = bytearray()
|
||||
tlv += der_encode(tag)
|
||||
39
client/src/ledger_app_clients/ethereum/utils.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from eth_account import Account
|
||||
from eth_account.messages import encode_defunct, encode_typed_data
|
||||
import rlp
|
||||
|
||||
|
||||
def get_selector_from_data(data: str) -> bytes:
|
||||
raw_data = bytes.fromhex(data[2:])
|
||||
return raw_data[:4]
|
||||
|
||||
|
||||
def recover_message(msg, vrs: tuple) -> bytes:
|
||||
if isinstance(msg, dict): # EIP-712
|
||||
smsg = encode_typed_data(full_message=msg)
|
||||
else: # EIP-191
|
||||
smsg = encode_defunct(primitive=msg)
|
||||
addr = Account.recover_message(smsg, vrs)
|
||||
return bytes.fromhex(addr[2:])
|
||||
|
||||
|
||||
def recover_transaction(tx_params, vrs: tuple) -> bytes:
|
||||
raw_tx = Account.create().sign_transaction(tx_params).rawTransaction
|
||||
prefix = bytes()
|
||||
if raw_tx[0] in [0x01, 0x02]:
|
||||
prefix = raw_tx[:1]
|
||||
raw_tx = raw_tx[len(prefix):]
|
||||
# v is returned on one byte only so it might have overflowed
|
||||
# in that case, we will reconstruct it to its full value
|
||||
if "chainId" in tx_params:
|
||||
trunc_chain_id = tx_params["chainId"]
|
||||
while trunc_chain_id.bit_length() > 32:
|
||||
trunc_chain_id >>= 8
|
||||
target = tx_params["chainId"] * 2 + 35
|
||||
trunc_target = trunc_chain_id * 2 + 35
|
||||
diff = vrs[0][0] - (trunc_target & 0xff)
|
||||
vrs = (target + diff, vrs[1], vrs[2])
|
||||
decoded = rlp.decode(raw_tx)
|
||||
reencoded = rlp.encode(decoded[:-3] + list(vrs))
|
||||
addr = Account.recover_transaction(prefix + reencoded)
|
||||
return bytes.fromhex(addr[2:])
|
||||
@@ -6,24 +6,24 @@ Application version 1.3.0 - 05th of July 2020
|
||||
## 1.3.0
|
||||
- Initial release
|
||||
|
||||
## About
|
||||
## About
|
||||
|
||||
This document described how a specific device UI for a smart contract can be added in the current version of the Ethereum application, before plugins are added
|
||||
|
||||
## Standard support
|
||||
|
||||
The applications already includes dedicated UI support for those specific contract calls :
|
||||
The applications already includes dedicated UI support for those specific contract calls :
|
||||
|
||||
* ERC 20 approve(address, uint256) - implementation in *src_features/erc20_approval*
|
||||
* ERC 20 transfer(address, uint256) - implementation in *src_features/signTx*
|
||||
|
||||
## Requirements
|
||||
|
||||
The following data is necessary for a specific contract support
|
||||
The following data is necessary for a specific contract support
|
||||
|
||||
* Smart contract ABI (at least for calls that are to be supported by the application)
|
||||
|
||||
The following data is optional for a specific contract support
|
||||
The following data is optional for a specific contract support
|
||||
|
||||
* Contract address (can be optional if supported by multiple instances)
|
||||
|
||||
@@ -38,60 +38,11 @@ The first four bytes of the call data for a function call specifies the function
|
||||
The following online tool can be used to compute selectors https://emn178.github.io/online-tools/keccak_256.html
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
* The total number of parameters of a contract call is 4 (as defined in *src/shared_context.h* for tokenContext_t.data, each parameter being encoded as an uint256). Additional parameters can be supported if not required to handle the displaying logic (see "Also handle exception that only need to process the beginning of data" in *src_features/signTx/logic_signTx.c* for CONTRACT_STARKWARE_VERIFY_ESCAPE and CONTRACT_STARKWARE_REGISTER)
|
||||
|
||||
* Non fixed size types (dynamic sized byte sequences, dynamic sized strings, variable length arrays) are not directly supported
|
||||
|
||||
## Sample implementation of a function call
|
||||
|
||||
This example describes how to implement a specific UI for a contract call. This exemple is using Starkex token deposit - *deposit(uint256 tokenId, uint256 vaultId, uint256 quantizedAmount)*
|
||||
|
||||
|
||||
* Implement the specific UI logic in a new *src_features* subdirectory - note that strings.common.maxFee will be provisioned with the maximum fee to pay for this call
|
||||
|
||||
```
|
||||
See src_features/stark_contract_deposit for this example
|
||||
```
|
||||
|
||||
* Compute the selector
|
||||
|
||||
|
||||
```
|
||||
keccak-256("deposit(uint256,uint256,uint256)") = 00aeef8a...
|
||||
```
|
||||
|
||||
* Add an entry to the contract_call_t enum of *src/shared_context.h* for this function call
|
||||
|
||||
```
|
||||
CONTRACT_STARKWARE_DEPOSIT_TOKEN
|
||||
```
|
||||
|
||||
* Check for the selector being called and the arguments size in *src_features/signTx/logic_signTx.c* customProcessor function - additional logic can be applied (quantumSet in this example)
|
||||
|
||||
```C
|
||||
if ((context->currentFieldLength == STARKWARE_DEPOSIT_TOKEN_DATA_SIZE) &&
|
||||
(memcmp(context->workBuffer, STARKWARE_DEPOSIT_TOKEN_ID, 4) == 0) &&
|
||||
quantumSet) {
|
||||
contractProvisioned = CONTRACT_STARKWARE_DEPOSIT_TOKEN;
|
||||
}
|
||||
```
|
||||
|
||||
* Call the dedicated UI at the end of the transaction parsing in *src_features/signTx/logic_signTx.c* finalizeParsing function
|
||||
|
||||
```C
|
||||
if (contractProvisioned == CONTRACT_STARKWARE_DEPOSIT_TOKEN) {
|
||||
ux_flow_init(0, ux_approval_starkware_deposit_flow, NULL);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
## Using ERC 20 token tickers
|
||||
|
||||
A UI implementation might want to convert an ERC 20 token contract address to a ticker for easier validation
|
||||
|
||||
2 tickers can be temporarily provisioned to the application by using the PROVIDE ERC 20 TOKEN INFORMATION APDU, described in *src_features/provideErc20TokenInformation* - the UI can then iterate on the provisioned tickers to display relevant information to the user
|
||||
2 tickers can be temporarily provisioned to the application by using the PROVIDE ERC 20 TOKEN INFORMATION APDU, described in *src_features/provideErc20TokenInformation* - the UI can then iterate on the provisioned tickers to display relevant information to the user
|
||||
|
||||
The same mechanism will be extended to support well known contract addresses in the future
|
||||
|
||||
|
||||
@@ -1,348 +0,0 @@
|
||||
Ethereum application : Starkware extensions
|
||||
============================================
|
||||
Ledger Firmware Team <hello@ledger.fr>
|
||||
Application version 1.5.0 - 4th of October 2020
|
||||
|
||||
## 1.3.0
|
||||
- Initial release
|
||||
|
||||
## 1.5.0
|
||||
- Update with Starkex v2 APIs
|
||||
|
||||
## About
|
||||
|
||||
This specification describes the APDU messages interface implementing the Starkware extensions for the Ethereum appilcation
|
||||
|
||||
## Modified general purpose APDUs
|
||||
|
||||
### GET APP CONFIGURATION
|
||||
|
||||
#### Description
|
||||
|
||||
This command returns specific application configuration
|
||||
|
||||
It is modified to notify Stark extensions support on flag 0x04
|
||||
|
||||
#### Coding
|
||||
|
||||
'Command'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le*
|
||||
| E0 | 06 | 00 | 00 | 00 | 04
|
||||
|==============================================================================================================================
|
||||
|
||||
'Input data'
|
||||
|
||||
None
|
||||
|
||||
'Output data'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Flags
|
||||
0x01 : arbitrary data signature enabled by user
|
||||
|
||||
0x02 : ERC 20 Token information needs to be provided externally
|
||||
|
||||
0x04 : Stark extensions are supported
|
||||
|
||||
0x08 : Stark protocol v2 is supported
|
||||
| 01
|
||||
| Application major version | 01
|
||||
| Application minor version | 01
|
||||
| Application patch version | 01
|
||||
|==============================================================================================================================
|
||||
|
||||
|
||||
## Additional APDUs
|
||||
|
||||
Additional APDUs use the APDU CLA F0
|
||||
|
||||
### GET STARK PUBLIC KEY
|
||||
|
||||
#### Description
|
||||
|
||||
This command returns the public Stark key (X and Y coordinates) for the given BIP 32 path.
|
||||
|
||||
The key can be optionally checked on the device before being returned - in that case, only the X coordinate is displayed, as this is what is used in the contract
|
||||
|
||||
#### Coding
|
||||
|
||||
'Command'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le*
|
||||
| F0 | 02 | 00 : return address
|
||||
|
||||
01 : display address and confirm before returning
|
||||
| 00 | variable | variable
|
||||
|==============================================================================================================================
|
||||
|
||||
'Input data'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Number of BIP 32 derivations to perform (max 10) | 1
|
||||
| First derivation index (big endian) | 4
|
||||
| ... | 4
|
||||
| Last derivation index (big endian) | 4
|
||||
|==============================================================================================================================
|
||||
|
||||
'Output data'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Stark key | 65
|
||||
|==============================================================================================================================
|
||||
|
||||
### SIGN STARK MESSAGE
|
||||
|
||||
#### Description
|
||||
|
||||
This command signs an order or a transfer on the Starkware curve.
|
||||
|
||||
The contract addressed associated to the token shall have be provisioned previously with the PROVIDE ERC 20 TOKEN INFORMATION command or this command will fail.
|
||||
|
||||
The quantum type for v2 messages is encoded as p1 for the PROVIDE QUANTUM command
|
||||
|
||||
#### Coding
|
||||
|
||||
'Command'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le*
|
||||
| F0 | 04 |
|
||||
01 : sign a Stark Order (protocol v1, handles ETH and regular ERC 20)
|
||||
|
||||
02 : sign a Stark Transfer (protocol v1, handles ETH and regular ERC 20)
|
||||
|
||||
03 : sign a Stark Order (since protocol v2)
|
||||
|
||||
04 : sign a Stark Transfer (since protocol v2)
|
||||
|
||||
05 : sign a Stark Conditional Transfer (since protocol v2)
|
||||
|
||||
| 00 | variable | variable
|
||||
|==============================================================================================================================
|
||||
|
||||
'Input data for a Stark Order (v1)'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Number of BIP 32 derivations to perform (max 10) | 1
|
||||
| First derivation index (big endian) | 4
|
||||
| ... | 4
|
||||
| Last derivation index (big endian) | 4
|
||||
| Contract address of the token to be sold (or 00..00 for ETH) | 20
|
||||
| Quantization of the token to be sold (big endian) | 32
|
||||
| Contract address of the token to be bought (or 00..00 for ETH) | 20
|
||||
| Quantization of the token to be bought (big endian) | 32
|
||||
| ID of the source vault (big endian encoded) | 4
|
||||
| ID of the destination vault (big endian encoded) | 4
|
||||
| Amount to be sold (big endian encoded) | 8
|
||||
| Amount to buy (big endian encoded) | 8
|
||||
| Transaction nonce (big endian encoded) | 4
|
||||
| Transaction timestamp (big endian encoded) | 4
|
||||
|==============================================================================================================================
|
||||
|
||||
'Input data for a Stark Transfer (v1)'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Number of BIP 32 derivations to perform (max 10) | 1
|
||||
| First derivation index (big endian) | 4
|
||||
| ... | 4
|
||||
| Last derivation index (big endian) | 4
|
||||
| Contract address of the token to be transferred (or 00..00 for ETH) | 20
|
||||
| Quantization of the token to be transferred (big endian) | 32
|
||||
| Token target public key | 32
|
||||
| ID of the source vault (big endian encoded) | 4
|
||||
| ID of the destination vault (big endian encoded) | 4
|
||||
| Amount to be transferred (big endian encoded) | 8
|
||||
| Transaction nonce (big endian encoded) | 4
|
||||
| Transaction timestamp (big endian encoded) | 4
|
||||
|==============================================================================================================================
|
||||
|
||||
'Input data for a Stark Order (v2)'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Number of BIP 32 derivations to perform (max 10) | 1
|
||||
| First derivation index (big endian) | 4
|
||||
| ... | 4
|
||||
| Last derivation index (big endian) | 4
|
||||
| Quantization type of the token to be sold | 1
|
||||
| Contract address of the token to be sold (or 00..00 for ETH) | 20
|
||||
| Quantization or Token ID of the token to be sold (big endian) | 32
|
||||
| Minting blob of the token to be sold (ignored if non mintable) | 32
|
||||
| Quantization type of the token to be bought | 1
|
||||
| Contract address of the token to be bought (or 00..00 for ETH) | 20
|
||||
| Quantization or Token ID of the token to be bought (big endian) | 32
|
||||
| Minting blob of the token to be bought (ignored if non mintable) | 32
|
||||
| ID of the source vault (big endian encoded) | 4
|
||||
| ID of the destination vault (big endian encoded) | 4
|
||||
| Amount to be sold (big endian encoded) | 8
|
||||
| Amount to buy (big endian encoded) | 8
|
||||
| Transaction nonce (big endian encoded) | 4
|
||||
| Transaction timestamp (big endian encoded) | 4
|
||||
|==============================================================================================================================
|
||||
|
||||
'Input data for a Stark Transfer (v2)'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Number of BIP 32 derivations to perform (max 10) | 1
|
||||
| First derivation index (big endian) | 4
|
||||
| ... | 4
|
||||
| Last derivation index (big endian) | 4
|
||||
| Quantization type of the token to be transferred | 1
|
||||
| Contract address of the token to be transferred (or 00..00 for ETH) | 20
|
||||
| Quantization or Token ID of the token to be transferred (big endian) | 32
|
||||
| Minting blob of the token to be transferred (ignored if non mintable) | 32
|
||||
| Token target public key | 32
|
||||
| ID of the source vault (big endian encoded) | 4
|
||||
| ID of the destination vault (big endian encoded) | 4
|
||||
| Amount to be transferred (big endian encoded) | 8
|
||||
| Transaction nonce (big endian encoded) | 4
|
||||
| Transaction timestamp (big endian encoded) | 4
|
||||
|==============================================================================================================================
|
||||
|
||||
'Input data for a Stark Conditional Transfer'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Number of BIP 32 derivations to perform (max 10) | 1
|
||||
| First derivation index (big endian) | 4
|
||||
| ... | 4
|
||||
| Last derivation index (big endian) | 4
|
||||
| Quantization type of the token to be transferred | 1
|
||||
| Contract address of the token to be transferred (or 00..00 for ETH) | 20
|
||||
| Quantization or Token ID of the token to be transferred (big endian) | 32
|
||||
| Minting blob of the token to be transferred (ignored if non mintable) | 32
|
||||
| Token target public key | 32
|
||||
| ID of the source vault (big endian encoded) | 4
|
||||
| ID of the destination vault (big endian encoded) | 4
|
||||
| Amount to be transferred (big endian encoded) | 8
|
||||
| Transaction nonce (big endian encoded) | 4
|
||||
| Transaction timestamp (big endian encoded) | 4
|
||||
| Conditional transfer fact (big endian) | 32
|
||||
| Conditional transfer L1 condition logic address | 20
|
||||
|==============================================================================================================================
|
||||
|
||||
'Output data'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| RFU (00) | 1
|
||||
| r | 32
|
||||
| s | 32
|
||||
|==============================================================================================================================
|
||||
|
||||
|
||||
### PROVIDES QUANTUM
|
||||
|
||||
#### Description
|
||||
|
||||
This command provides quantization data used to compute a tokenId and provide additional information to the user before signing a transaction performing a deposit or withdrawal call on a Stark powered smart contract.
|
||||
|
||||
It shall be called following a PROVIDE ERC 20 TOKEN INFORMATION command called for the associated contract
|
||||
|
||||
#### Coding
|
||||
|
||||
'Command'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le*
|
||||
| F0 | 08 |
|
||||
00 : legacy (protocol v1, handles ETH and regular ERC 20)
|
||||
|
||||
01 : quantum encoded for ETH (since protocol v2)
|
||||
|
||||
02 : quantum encoded for a regular ERC 20 (since protocol v2)
|
||||
|
||||
03 : quantum encoded for a regular ERC 721 (since protocol v2)
|
||||
|
||||
04 : quantum encoded for a mintable ERC 20 (since protocol v2)
|
||||
|
||||
05 : quantum encoded for a mintable ERC 721 (since protocol v2)
|
||||
|
||||
| 00 | variable | variable
|
||||
|==============================================================================================================================
|
||||
|
||||
'Legacy Input data'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Contract address used in the next transaction | 20
|
||||
| Quantization to be used in the next transaction | 32
|
||||
|==============================================================================================================================
|
||||
|
||||
'v2 Input data'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Contract address used in the next transaction (ignored for ETH) | 20
|
||||
| Quantization to be used in the next transaction (ignored for ERC 721s) | 32
|
||||
| Minting blob to be used in the next transaction (ignored for non mintable) | 32
|
||||
|==============================================================================================================================
|
||||
|
||||
'Output data'
|
||||
|
||||
None
|
||||
|
||||
### UNSAFE SIGN
|
||||
|
||||
#### Description
|
||||
|
||||
This command signs an arbitrary hash on the Starkware Curve after presenting the hash to the user. It is intended for speed of execution in case an unknown Stark model is pushed and should be avoided as much as possible.
|
||||
|
||||
#### Coding
|
||||
|
||||
'Command'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le*
|
||||
| F0 | 0A |
|
||||
00
|
||||
| 00 | variable | variable
|
||||
|==============================================================================================================================
|
||||
|
||||
'Input data'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Number of BIP 32 derivations to perform (max 10) | 1
|
||||
| First derivation index (big endian) | 4
|
||||
| ... | 4
|
||||
| Last derivation index (big endian) | 4
|
||||
| Hash to sign | 32
|
||||
|==============================================================================================================================
|
||||
|
||||
'Output data'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| RFU (00) | 1
|
||||
| r | 32
|
||||
| s | 32
|
||||
|==============================================================================================================================
|
||||
|
||||
@@ -87,6 +87,7 @@ The address can be optionally checked on the device before being returned.
|
||||
| First derivation index (big endian) | 4
|
||||
| ... | 4
|
||||
| Last derivation index (big endian) | 4
|
||||
| Chain ID (big endian) (optional) | 8
|
||||
|==============================================================================================================================
|
||||
|
||||
'Output data'
|
||||
@@ -527,7 +528,7 @@ The plugin names `ERC20`, `ERC721` and `ERC1155` are reserved. Additional plugin
|
||||
|
||||
The signature is computed on
|
||||
|
||||
type || version || len(pluginName) || pluginName || address || selector || chainId || keyId || algorithmId || len(signature) || signature
|
||||
type || version || len(pluginName) || pluginName || address || selector || chainId || keyId || algorithmId
|
||||
|
||||
#### Coding
|
||||
|
||||
|
||||
@@ -1,315 +1,313 @@
|
||||
Ethereum application Plugins : Technical Specifications
|
||||
=======================================================
|
||||
Ledger Firmware Team <hello@ledger.fr>
|
||||
Specification version 1.0 - 24th of September 2020
|
||||
|
||||
|
||||
## 1.0
|
||||
- Initial release
|
||||
|
||||
## About
|
||||
|
||||
This specification describes the plugin interface used to display a specific UI on device for Ethereum smart contracts.
|
||||
|
||||
Feel free to checkout the ParaSwap plugin to see an actual implementation. Link: https://github.com/LedgerHQ/app-ethereum/blob/named-external-plugins/doc/ethapp_plugins.asc .
|
||||
|
||||
## Flow overview
|
||||
|
||||
When signing an Ethereum transaction containing data, the Ethereum application looks for a plugin using .either a selector list or the contract address.
|
||||
|
||||
If a plugin is found, each network serialized data field (32 bytes) is passed to the plugin along with the field offset. The plugin can decide to stop the signature process if a data field isn't expected
|
||||
|
||||
After all fields have been received, the plugin can report to the Ethereum application whether the full data is accepted, and the user interface model that'll be used to display the data
|
||||
|
||||
### Amount/Address user interface
|
||||
|
||||
In this model, the generic (without data) transaction display is used, with the amount and destination address replaced by data provided by the plugin
|
||||
|
||||
### Generic user interface
|
||||
|
||||
In this model, the plugin first reports a number of screens (2 lines of text, the second line being scrollable) to be displayed
|
||||
|
||||
The Ethereum application will request each screen to be displayed to the plugin and let the user browse through them.
|
||||
|
||||
The first screen being displayed is always a description of the plugin being used (name and version reported by the plugin), and the last screens include the transaction fees in ETH and a confirmation prompt
|
||||
|
||||
### Code flow
|
||||
|
||||
The plugin interfacing logic is described in _src/eth_plugin_interface.h_
|
||||
|
||||
The plugin common dispatcher is found in _src/eth_plugin_handler.c_
|
||||
|
||||
The plugin generic UI dispatcher is found in _src/eth_plugin_ui.c_
|
||||
|
||||
Sample internal plugins are provided in _src_plugins/_
|
||||
|
||||
## Creating a plugin
|
||||
|
||||
### Creating an internal plugin
|
||||
|
||||
Internal plugins are triggered on specific selectors. You can modify _src/eth_plugin_internal.c_ to add your mapping.
|
||||
|
||||
Other specific mappings can be also added by modifying the common dispatcher
|
||||
|
||||
### Creating an external plugin
|
||||
|
||||
An external plugin is a library application named after the base64 encoding of the 20 bytes smart contract address
|
||||
|
||||
## Detailed flow messages
|
||||
|
||||
### Generic fields
|
||||
|
||||
The following generic fields are present in all messages :
|
||||
|
||||
* pluginSharedRW : scratch objects and utilities available to the plugin (can be read and written)
|
||||
|
||||
* pluginSharedRO : transaction data available to the plugin (can only be read)
|
||||
|
||||
* pluginContext : arbitrary data blob holding the plugin context, to be set and used by the plugin
|
||||
|
||||
* result : return code set by the plugin following the message processing
|
||||
|
||||
### ETH_PLUGIN_INIT_CONTRACT
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethPluginInitContract_t {
|
||||
|
||||
// in
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
uint8_t *pluginContext;
|
||||
uint32_t pluginContextLength;
|
||||
uint8_t *selector; // 4 bytes selector
|
||||
uint32_t dataSize;
|
||||
|
||||
char *alias; // 29 bytes alias if ETH_PLUGIN_RESULT_OK_ALIAS set
|
||||
|
||||
uint8_t result;
|
||||
|
||||
} ethPluginInitContract_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent when the selector of the data has been parsed. The following specific fields are filled when the plugin is called :
|
||||
|
||||
* pluginContextLength : length of the data field available to store the plugin context
|
||||
* selector : 4 bytes selector of the data field
|
||||
* dataSize : size in bytes of the data field
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
* ETH_PLUGIN_RESULT_OK_ALIAS : if a base64 encoded alias of another plugin to call is copied to the _alias_ field. In this case, the dispatcher will follow the alias chain, and the original plugin will only be called to retrieve its name when using a generic user interface
|
||||
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
|
||||
|
||||
### ETH_PLUGIN_PROVIDE_PARAMETER
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethPluginProvideParameter_t {
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
uint8_t *pluginContext;
|
||||
uint8_t *parameter; // 32 bytes parameter
|
||||
uint32_t parameterOffset;
|
||||
|
||||
uint8_t result;
|
||||
|
||||
} ethPluginProvideParameter_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent when a new 32 bytes component of the data field is available. The following specific fields are filled when the plugin is called :
|
||||
|
||||
* parameter : pointer to the 32 bytes parameter being parsed
|
||||
* parameterOffset : offset to this parameter from the beginning of the data field (starts at 4, following the selector)
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
|
||||
|
||||
There are already defined functions to extract data from a parameter:
|
||||
[source,C]
|
||||
----
|
||||
void copy_address(uint8_t* dst, const uint8_t* parameter, uint8_t dst_size);
|
||||
void copy_parameter(uint8_t* dst, const uint8_t* parameter, uint8_t dst_size);
|
||||
|
||||
// Get the value from the beginning of the parameter (right to left) and check if the rest of it is zero
|
||||
bool U2BE_from_parameter(const uint8_t* parameter, uint16_t* value);
|
||||
bool U4BE_from_parameter(const uint8_t* parameter, uint32_t* value);
|
||||
----
|
||||
|
||||
### ETH_PLUGIN_FINALIZE
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethPluginFinalize_t {
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
uint8_t *pluginContext;
|
||||
|
||||
uint8_t *itemLookup1; // set by the plugin if a token or an nft should be looked up
|
||||
uint8_t *itemLookup2;
|
||||
|
||||
uint8_t *amount; // set an uint256 pointer if uiType is UI_AMOUNT_ADDRESS
|
||||
uint8_t *address; // set to the destination address if uiType is UI_AMOUNT_ADDRESS. Set to the user's address if uiType is UI_TYPE_GENERIC
|
||||
|
||||
uint8_t uiType;
|
||||
uint8_t numScreens; // ignored if uiType is UI_AMOUNT_ADDRESS
|
||||
uint8_t result;
|
||||
|
||||
} ethPluginFinalize_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent when the data field has been fully parsed. The following specific fields can be filled by the plugin :
|
||||
|
||||
* itemLookup1 : the pointer shall be set to a 20 bytes address to look up an ERC20 token or NFT if needed by the plugin
|
||||
* itemLookup2 : the pointer shall be set to a 20 bytes address to look up an ERC20 token or NFT if needed by the plugin
|
||||
* uiType : set to either ETH_UI_TYPE_AMOUNT_ADDRESS for an amount/address UI or ETH_UI_TYPE_GENERIC for a generic UI
|
||||
|
||||
The following specific fields are filled by the plugin when returning an amount/address UI :
|
||||
|
||||
* amount : set to a pointer to a 256 bits number
|
||||
* address : set to a pointer to a 20 bytes address
|
||||
|
||||
The following specific fields are filled by the plugin when returning a generic UI :
|
||||
|
||||
* numScreens : number of screens handled by the plugin
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
|
||||
|
||||
### ETH_PLUGIN_PROVIDE_INFO
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethPluginProvideInfo_t {
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
uint8_t *pluginContext;
|
||||
|
||||
union extraInfo *item1; // set by the ETH application, to be saved by the plugin
|
||||
union extraInfo *item2;
|
||||
|
||||
uint8_t additionalScreens; // Used by the plugin if it needs to display additional screens based on the information received.
|
||||
|
||||
uint8_t result;
|
||||
|
||||
} ethPluginProvideInfo_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent if an information lookup was required by the plugin when parsing a finalize message. The following specific fields are filled when the plugin is called :
|
||||
|
||||
* item1 : pointer to an union matching itemLookup1, or NULL if not found
|
||||
* item2 : pointer to an union matching itemLookup2, or NULL if not found
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
|
||||
|
||||
### ETH_PLUGIN_QUERY_CONTRACT_ID
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethQueryContractID_t {
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
uint8_t *pluginContext;
|
||||
|
||||
char *name;
|
||||
uint32_t nameLength;
|
||||
char *version;
|
||||
uint32_t versionLength;
|
||||
|
||||
uint8_t result;
|
||||
|
||||
} ethQueryContractID_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent after the parsing finalization and information lookups if requested if a generic UI is used. The following specific fields are provided when the plugin is called :
|
||||
|
||||
* name : pointer to the name of the plugin, to be filled by the plugin
|
||||
* nameLength : maximum name length
|
||||
* version : pointer to the version of the plugin, to be filled by the plugin
|
||||
* versionLength : maximum version length
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
|
||||
### ETH_PLUGIN_QUERY_CONTRACT_UI
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethQueryContractUI_t {
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
union extraInfo_t *item1;
|
||||
union extraInfo_t *item2;
|
||||
char network_ticker[MAX_TICKER_LEN];
|
||||
uint8_t *pluginContext;
|
||||
uint8_t screenIndex;
|
||||
char *title;
|
||||
uint32_t titleLength;
|
||||
char *msg;
|
||||
uint32_t msgLength;
|
||||
|
||||
uint8_t result;
|
||||
|
||||
} ethQueryContractUI_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent when a plugin screen shall be displayed if a generic UI is used. The following specific fields are provided when the plugin is called :
|
||||
|
||||
|
||||
* item1 : pointer to token / nft information
|
||||
* item2 : pointer to token / nft information
|
||||
* network_ticker : string that holds the network ticker
|
||||
* screenIndex : index of the screen to display, starting from 0
|
||||
* title : pointer to the first line of the screen, to be filled by the plugin
|
||||
* titleLength : maximum title length
|
||||
* msg : pointer to the second line of the screen, to be filled by the plugin
|
||||
* msgLength : maximum msg length
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
|
||||
## Caveats
|
||||
|
||||
When setting a pointer from the plugin space, make sure to use an address that will be accessible from the Ethereum application (typically in the plugin RAM context, *not* on the plugin stack)
|
||||
|
||||
Do not use data types that need to be aligned (such as uint32_t) in the plugin context.
|
||||
|
||||
## TODOs
|
||||
|
||||
Provide a sample callback mechanism for common plugin actions (amount to string, 256 bits number multiplication ...) to avoid duplicating code in the plugin space
|
||||
|
||||
Provide external plugins samples
|
||||
|
||||
Fully support Starkware as an independant application (APDU logic added)
|
||||
|
||||
Support extra flags for the generic UI (fast confirmation on first screen, ...)
|
||||
|
||||
Support extra plugin provisioning (signed list of associated smart contract addresses, ...)
|
||||
Ethereum application Plugins : Technical Specifications
|
||||
=======================================================
|
||||
Ledger Firmware Team <hello@ledger.fr>
|
||||
Specification version 1.0 - 24th of September 2020
|
||||
|
||||
|
||||
## 1.0
|
||||
- Initial release
|
||||
|
||||
## About
|
||||
|
||||
This specification describes the plugin interface used to display a specific UI on device for Ethereum smart contracts.
|
||||
|
||||
Feel free to checkout the ParaSwap plugin to see an actual implementation. Link: https://github.com/LedgerHQ/app-ethereum/blob/named-external-plugins/doc/ethapp_plugins.asc .
|
||||
|
||||
## Flow overview
|
||||
|
||||
When signing an Ethereum transaction containing data, the Ethereum application looks for a plugin using .either a selector list or the contract address.
|
||||
|
||||
If a plugin is found, each network serialized data field (32 bytes) is passed to the plugin along with the field offset. The plugin can decide to stop the signature process if a data field isn't expected
|
||||
|
||||
After all fields have been received, the plugin can report to the Ethereum application whether the full data is accepted, and the user interface model that'll be used to display the data
|
||||
|
||||
### Amount/Address user interface
|
||||
|
||||
In this model, the generic (without data) transaction display is used, with the amount and destination address replaced by data provided by the plugin
|
||||
|
||||
### Generic user interface
|
||||
|
||||
In this model, the plugin first reports a number of screens (2 lines of text, the second line being scrollable) to be displayed
|
||||
|
||||
The Ethereum application will request each screen to be displayed to the plugin and let the user browse through them.
|
||||
|
||||
The first screen being displayed is always a description of the plugin being used (name and version reported by the plugin), and the last screens include the transaction fees in ETH and a confirmation prompt
|
||||
|
||||
### Code flow
|
||||
|
||||
The plugin interfacing logic is described in _src/eth_plugin_interface.h_
|
||||
|
||||
The plugin common dispatcher is found in _src/eth_plugin_handler.c_
|
||||
|
||||
The plugin generic UI dispatcher is found in _src/eth_plugin_ui.c_
|
||||
|
||||
Sample internal plugins are provided in _src_plugins/_
|
||||
|
||||
## Creating a plugin
|
||||
|
||||
### Creating an internal plugin
|
||||
|
||||
Internal plugins are triggered on specific selectors. You can modify _src/eth_plugin_internal.c_ to add your mapping.
|
||||
|
||||
Other specific mappings can be also added by modifying the common dispatcher
|
||||
|
||||
### Creating an external plugin
|
||||
|
||||
An external plugin is a library application named after the base64 encoding of the 20 bytes smart contract address
|
||||
|
||||
## Detailed flow messages
|
||||
|
||||
### Generic fields
|
||||
|
||||
The following generic fields are present in all messages :
|
||||
|
||||
* pluginSharedRW : scratch objects and utilities available to the plugin (can be read and written)
|
||||
|
||||
* pluginSharedRO : transaction data available to the plugin (can only be read)
|
||||
|
||||
* pluginContext : arbitrary data blob holding the plugin context, to be set and used by the plugin
|
||||
|
||||
* result : return code set by the plugin following the message processing
|
||||
|
||||
### ETH_PLUGIN_INIT_CONTRACT
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethPluginInitContract_t {
|
||||
|
||||
// in
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
uint8_t *pluginContext;
|
||||
uint32_t pluginContextLength;
|
||||
uint8_t *selector; // 4 bytes selector
|
||||
uint32_t dataSize;
|
||||
|
||||
char *alias; // 29 bytes alias if ETH_PLUGIN_RESULT_OK_ALIAS set
|
||||
|
||||
uint8_t result;
|
||||
|
||||
} ethPluginInitContract_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent when the selector of the data has been parsed. The following specific fields are filled when the plugin is called :
|
||||
|
||||
* pluginContextLength : length of the data field available to store the plugin context
|
||||
* selector : 4 bytes selector of the data field
|
||||
* dataSize : size in bytes of the data field
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
* ETH_PLUGIN_RESULT_OK_ALIAS : if a base64 encoded alias of another plugin to call is copied to the _alias_ field. In this case, the dispatcher will follow the alias chain, and the original plugin will only be called to retrieve its name when using a generic user interface
|
||||
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
|
||||
|
||||
### ETH_PLUGIN_PROVIDE_PARAMETER
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethPluginProvideParameter_t {
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
uint8_t *pluginContext;
|
||||
uint8_t *parameter; // 32 bytes parameter
|
||||
uint32_t parameterOffset;
|
||||
|
||||
uint8_t result;
|
||||
|
||||
} ethPluginProvideParameter_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent when a new 32 bytes component of the data field is available. The following specific fields are filled when the plugin is called :
|
||||
|
||||
* parameter : pointer to the 32 bytes parameter being parsed
|
||||
* parameterOffset : offset to this parameter from the beginning of the data field (starts at 4, following the selector)
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
|
||||
|
||||
There are already defined functions to extract data from a parameter:
|
||||
[source,C]
|
||||
----
|
||||
void copy_address(uint8_t* dst, const uint8_t* parameter, uint8_t dst_size);
|
||||
void copy_parameter(uint8_t* dst, const uint8_t* parameter, uint8_t dst_size);
|
||||
|
||||
// Get the value from the beginning of the parameter (right to left) and check if the rest of it is zero
|
||||
bool U2BE_from_parameter(const uint8_t* parameter, uint16_t* value);
|
||||
bool U4BE_from_parameter(const uint8_t* parameter, uint32_t* value);
|
||||
----
|
||||
|
||||
### ETH_PLUGIN_FINALIZE
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethPluginFinalize_t {
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
uint8_t *pluginContext;
|
||||
|
||||
uint8_t *itemLookup1; // set by the plugin if a token or an nft should be looked up
|
||||
uint8_t *itemLookup2;
|
||||
|
||||
uint8_t *amount; // set an uint256 pointer if uiType is UI_AMOUNT_ADDRESS
|
||||
uint8_t *address; // set to the destination address if uiType is UI_AMOUNT_ADDRESS. Set to the user's address if uiType is UI_TYPE_GENERIC
|
||||
|
||||
uint8_t uiType;
|
||||
uint8_t numScreens; // ignored if uiType is UI_AMOUNT_ADDRESS
|
||||
uint8_t result;
|
||||
|
||||
} ethPluginFinalize_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent when the data field has been fully parsed. The following specific fields can be filled by the plugin :
|
||||
|
||||
* itemLookup1 : the pointer shall be set to a 20 bytes address to look up an ERC20 token or NFT if needed by the plugin
|
||||
* itemLookup2 : the pointer shall be set to a 20 bytes address to look up an ERC20 token or NFT if needed by the plugin
|
||||
* uiType : set to either ETH_UI_TYPE_AMOUNT_ADDRESS for an amount/address UI or ETH_UI_TYPE_GENERIC for a generic UI
|
||||
|
||||
The following specific fields are filled by the plugin when returning an amount/address UI :
|
||||
|
||||
* amount : set to a pointer to a 256 bits number
|
||||
* address : set to a pointer to a 20 bytes address
|
||||
|
||||
The following specific fields are filled by the plugin when returning a generic UI :
|
||||
|
||||
* numScreens : number of screens handled by the plugin
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
|
||||
|
||||
### ETH_PLUGIN_PROVIDE_INFO
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethPluginProvideInfo_t {
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
uint8_t *pluginContext;
|
||||
|
||||
union extraInfo *item1; // set by the ETH application, to be saved by the plugin
|
||||
union extraInfo *item2;
|
||||
|
||||
uint8_t additionalScreens; // Used by the plugin if it needs to display additional screens based on the information received.
|
||||
|
||||
uint8_t result;
|
||||
|
||||
} ethPluginProvideInfo_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent if an information lookup was required by the plugin when parsing a finalize message. The following specific fields are filled when the plugin is called :
|
||||
|
||||
* item1 : pointer to an union matching itemLookup1, or NULL if not found
|
||||
* item2 : pointer to an union matching itemLookup2, or NULL if not found
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
|
||||
|
||||
### ETH_PLUGIN_QUERY_CONTRACT_ID
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethQueryContractID_t {
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
uint8_t *pluginContext;
|
||||
|
||||
char *name;
|
||||
uint32_t nameLength;
|
||||
char *version;
|
||||
uint32_t versionLength;
|
||||
|
||||
uint8_t result;
|
||||
|
||||
} ethQueryContractID_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent after the parsing finalization and information lookups if requested if a generic UI is used. The following specific fields are provided when the plugin is called :
|
||||
|
||||
* name : pointer to the name of the plugin, to be filled by the plugin
|
||||
* nameLength : maximum name length
|
||||
* version : pointer to the version of the plugin, to be filled by the plugin
|
||||
* versionLength : maximum version length
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
|
||||
### ETH_PLUGIN_QUERY_CONTRACT_UI
|
||||
|
||||
[source,C]
|
||||
----
|
||||
|
||||
typedef struct ethQueryContractUI_t {
|
||||
|
||||
ethPluginSharedRW_t *pluginSharedRW;
|
||||
ethPluginSharedRO_t *pluginSharedRO;
|
||||
union extraInfo_t *item1;
|
||||
union extraInfo_t *item2;
|
||||
char network_ticker[MAX_TICKER_LEN];
|
||||
uint8_t *pluginContext;
|
||||
uint8_t screenIndex;
|
||||
char *title;
|
||||
uint32_t titleLength;
|
||||
char *msg;
|
||||
uint32_t msgLength;
|
||||
|
||||
uint8_t result;
|
||||
|
||||
} ethQueryContractUI_t;
|
||||
|
||||
----
|
||||
|
||||
This message is sent when a plugin screen shall be displayed if a generic UI is used. The following specific fields are provided when the plugin is called :
|
||||
|
||||
|
||||
* item1 : pointer to token / nft information
|
||||
* item2 : pointer to token / nft information
|
||||
* network_ticker : string that holds the network ticker
|
||||
* screenIndex : index of the screen to display, starting from 0
|
||||
* title : pointer to the first line of the screen, to be filled by the plugin
|
||||
* titleLength : maximum title length
|
||||
* msg : pointer to the second line of the screen, to be filled by the plugin
|
||||
* msgLength : maximum msg length
|
||||
|
||||
The following return codes are expected, any other will abort the signing process :
|
||||
|
||||
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
|
||||
|
||||
## Caveats
|
||||
|
||||
When setting a pointer from the plugin space, make sure to use an address that will be accessible from the Ethereum application (typically in the plugin RAM context, *not* on the plugin stack)
|
||||
|
||||
Do not use data types that need to be aligned (such as uint32_t) in the plugin context.
|
||||
|
||||
## TODOs
|
||||
|
||||
Provide a sample callback mechanism for common plugin actions (amount to string, 256 bits number multiplication ...) to avoid duplicating code in the plugin space
|
||||
|
||||
Provide external plugins samples
|
||||
|
||||
Support extra flags for the generic UI (fast confirmation on first screen, ...)
|
||||
|
||||
Support extra plugin provisioning (signed list of associated smart contract addresses, ...)
|
||||
|
||||
BIN
glyphs/stax_chain_100_64px.gif
Normal file
|
After Width: | Height: | Size: 734 B |
BIN
glyphs/stax_chain_10200_64px.gif
Normal file
|
After Width: | Height: | Size: 734 B |
|
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
BIN
glyphs/stax_chain_10507_64px.gif
Normal file
|
After Width: | Height: | Size: 718 B |
BIN
glyphs/stax_chain_106_64px.gif
Normal file
|
After Width: | Height: | Size: 444 B |
BIN
glyphs/stax_chain_1088_64px.gif
Normal file
|
After Width: | Height: | Size: 720 B |
|
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 287 B |
BIN
glyphs/stax_chain_10_64px.gif
Normal file
|
After Width: | Height: | Size: 654 B |
BIN
glyphs/stax_chain_1101_64px.gif
Normal file
|
After Width: | Height: | Size: 543 B |
1
glyphs/stax_chain_11155111_64px.gif
Symbolic link
@@ -0,0 +1 @@
|
||||
stax_chain_1_64px.gif
|
||||
BIN
glyphs/stax_chain_1116_64px.gif
Normal file
|
After Width: | Height: | Size: 706 B |
BIN
glyphs/stax_chain_11297108109_64px.gif
Normal file
|
After Width: | Height: | Size: 448 B |
|
Before Width: | Height: | Size: 212 B After Width: | Height: | Size: 212 B |
|
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 622 B After Width: | Height: | Size: 622 B |
|
Before Width: | Height: | Size: 681 B After Width: | Height: | Size: 681 B |
|
Before Width: | Height: | Size: 199 B After Width: | Height: | Size: 199 B |
|
Before Width: | Height: | Size: 584 B After Width: | Height: | Size: 584 B |
|
Before Width: | Height: | Size: 210 B After Width: | Height: | Size: 210 B |
1
glyphs/stax_chain_17000_64px.gif
Symbolic link
@@ -0,0 +1 @@
|
||||
stax_chain_1_64px.gif
|
||||
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 966 B |
BIN
glyphs/stax_chain_1907_64px.gif
Normal file
|
After Width: | Height: | Size: 725 B |
BIN
glyphs/stax_chain_196_64px.gif
Normal file
|
After Width: | Height: | Size: 271 B |
|
Before Width: | Height: | Size: 649 B After Width: | Height: | Size: 649 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 251 B After Width: | Height: | Size: 251 B |
|
Before Width: | Height: | Size: 569 B After Width: | Height: | Size: 569 B |
|
Before Width: | Height: | Size: 886 B After Width: | Height: | Size: 886 B |
|
Before Width: | Height: | Size: 389 B After Width: | Height: | Size: 389 B |
|
Before Width: | Height: | Size: 389 B After Width: | Height: | Size: 389 B |
BIN
glyphs/stax_chain_2222_64px.gif
Normal file
|
After Width: | Height: | Size: 470 B |
|
Before Width: | Height: | Size: 914 B After Width: | Height: | Size: 914 B |
|
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 217 B |
BIN
glyphs/stax_chain_245022926_64px.gif
Normal file
|
After Width: | Height: | Size: 587 B |
BIN
glyphs/stax_chain_245022934_64px.gif
Normal file
|
After Width: | Height: | Size: 587 B |
|
Before Width: | Height: | Size: 997 B After Width: | Height: | Size: 997 B |
|
Before Width: | Height: | Size: 985 B After Width: | Height: | Size: 985 B |
|
Before Width: | Height: | Size: 836 B After Width: | Height: | Size: 836 B |
|
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 340 B |
|
Before Width: | Height: | Size: 310 B After Width: | Height: | Size: 310 B |
BIN
glyphs/stax_chain_250_64px.gif
Normal file
|
After Width: | Height: | Size: 661 B |
BIN
glyphs/stax_chain_25_64px.gif
Normal file
|
After Width: | Height: | Size: 610 B |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 354 B |
BIN
glyphs/stax_chain_288_64px.gif
Normal file
|
After Width: | Height: | Size: 504 B |
|
Before Width: | Height: | Size: 314 B After Width: | Height: | Size: 314 B |
|
Before Width: | Height: | Size: 384 B After Width: | Height: | Size: 384 B |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 265 B |
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 293 B |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
BIN
glyphs/stax_chain_321_64px.gif
Normal file
|
After Width: | Height: | Size: 835 B |
|
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 342 B |
BIN
glyphs/stax_chain_369_64px.gif
Normal file
|
After Width: | Height: | Size: 596 B |
BIN
glyphs/stax_chain_39797_64px.gif
Normal file
|
After Width: | Height: | Size: 421 B |
|
Before Width: | Height: | Size: 248 B After Width: | Height: | Size: 248 B |
|
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 228 B |
BIN
glyphs/stax_chain_40_64px.gif
Normal file
|
After Width: | Height: | Size: 880 B |
BIN
glyphs/stax_chain_4201_64px.gif
Normal file
|
After Width: | Height: | Size: 493 B |
BIN
glyphs/stax_chain_42161_64px.gif
Normal file
|
After Width: | Height: | Size: 798 B |
BIN
glyphs/stax_chain_42220_64px.gif
Normal file
|
After Width: | Height: | Size: 836 B |
BIN
glyphs/stax_chain_42_64px.gif
Normal file
|
After Width: | Height: | Size: 493 B |
BIN
glyphs/stax_chain_43114_64px.gif
Normal file
|
After Width: | Height: | Size: 586 B |
BIN
glyphs/stax_chain_44787_64px.gif
Normal file
|
After Width: | Height: | Size: 836 B |