Skip to content

Commit

Permalink
[SDK] Initial commit copying from Aptos-core
Browse files Browse the repository at this point in the history
  • Loading branch information
gregnazario committed Nov 7, 2023
1 parent bcd6ac3 commit f256f1a
Show file tree
Hide file tree
Showing 48 changed files with 9,308 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[flake8]
max-line-length = 88
select = C,E,F,W,B,B9
ignore = E203, E501, W503
exclude = __init__.py
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Other stuff
*.egg.info
/myvenv

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -158,3 +162,4 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Aptos Python SDK Changelog

All notable changes to the Aptos Python SDK will be captured in this file. This changelog is written by hand for now.

## 0.8.0
- Add support for SingleKeyAuthenicatoin component of AIP-55
- Add support for Secp256k1 Ecdsa of AIP-49
- Add support for Sponsored transactions of AIP-39 and AIP-53
- Improved support for MultiEd25519

## 0.7.0
- **[Breaking Change]**: The `from_str` function on `AccountAddress` has been updated to conform to the strict parsing described by [AIP-40](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md). For the relaxed parsing behavior of this function prior to this change, use `AccountAddress.from_str_relaxed`.
- **[Breaking Change]**: Rewrote the large package publisher to support large modules too
- **[Breaking Change]**: Delete sync client
- **[Breaking Change]**: Removed the `hex` function from `AccountAddress`. Instead of `addr.hex()` use `str(addr)`.
- **[Breaking Change]**: The string representation of `AccountAddress` now conforms to [AIP-40](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md).
- **[Breaking Change]**: `AccountAddress.from_hex` and `PrivateKey.from_hex` have been renamed to `from_str`.
- Port remaining sync examples to async (hello-blockchain, multisig, your-coin)
- Updated token client to use events to acquire minted tokens
- Update many dependencies and set Python 3.8.1 as the minimum requirement
- Add support for an experimental chunked uploader
- Add experimental support for the Aptos CLI enabling local end-to-end testing, package building, and package integration tests

## 0.6.4
- Change sync client library from httpX to requests due to latency concerns.

## 0.6.2
- Added custom header "x-aptos-client" to both sync/async RestClient

## 0.6.1
- Updated package manifest.

## 0.6.0
- Add token client.
- Add support for generating account addresses.
- Add support for http2
- Add async client

18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Contributing Guide
## Publishing
To publish the SDK, follow these steps.

First, make sure you have updated the changelog and bumped the SDK version if necessary.

Configure Poetry with the PyPi credentials:

```
poetry config pypi-token.pypi <token>
```

You can get the token from our credential management system, search for PyPi.

Build and publish:
```
poetry publish --build
```
32 changes: 32 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright © Aptos Foundation
# SPDX-License-Identifier: Apache-2.0

test:
poetry run python -m unittest discover -s aptos_sdk/ -p '*.py' -t ..

test-coverage:
poetry run python -m coverage run -m unittest discover -s aptos_sdk/ -p '*.py' -t ..
poetry run python -m coverage report

fmt:
find ./examples ./aptos_sdk . -type f -name "*.py" | xargs poetry run autoflake -i -r --remove-all-unused-imports --remove-unused-variables --ignore-init-module-imports
poetry run isort aptos_sdk examples
poetry run black aptos_sdk examples

lint:
poetry run mypy aptos_sdk examples
poetry run flake8 aptos_sdk examples

examples:
poetry run python -m examples.aptos_token
poetry run python -m examples.read_aggregator
poetry run python -m examples.simple_nft
poetry run python -m examples.simple_aptos_token
poetry run python -m examples.simulate_transfer_coin
poetry run python -m examples.transfer_coin
poetry run python -m examples.transfer_two_by_two

examples_cli:
poetry run python -m unittest -b examples.integration_test

.PHONY: examples fmt lint test
91 changes: 89 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,89 @@
# aptos-python-sdk
Aptos Python SDK
# Aptos Python SDK
[![Discord][discord-image]][discord-url]
[![PyPI Package Version][pypi-image-version]][pypi-url]
[![PyPI Package Downloads][pypi-image-downloads]][pypi-url]

This provides basic functionalities to interact with [Aptos](https:/github.com/aptos-labs/aptos-core/). Get started [here](https://aptos.dev/guides/system-integrators-guide/#getting-started).

Currently this is still in development and may not be suitable for production purposes.

Note: The sync client is deprecated, please only start new projects using the async client. Feature contributions to the sync client will be rejected.

## Requirements
This SDK uses [Poetry](https://python-poetry.org/docs/#installation) for packaging and dependency management:

```
curl -sSL https://install.python-poetry.org | python3 -
poetry install
```

## Unit testing
```bash
make test
```

## E2E testing and Using the Aptos CLI

* Download the [Aptos CLI](https://aptos.dev/tools/aptos-cli/install-cli/).
* Set the environment variable `APTOS_CLI_PATH` to the full path of the CLI.
* `make examples_cli`

We of course allow you to do this a bit more manually by:

First, run a local testnet (run this from the root of aptos-core):

```bash
cargo run -p aptos -- node run-local-testnet --force-restart --assume-yes
```

Next, tell the end-to-end tests to talk to this locally running testnet:

```bash
export APTOS_NODE_URL="http://127.0.0.1:8080/v1"
export APTOS_FAUCET_URL="http://127.0.0.1:8081"
```

Finally run the tests:

```bash
make examples
```

Note: These end-to-end tests are tested against a node built from the same commit as part of CI, not devnet. For examples tested against devnet, see `developer-docs-site/static/examples/python/` from the root of the repo.

## Autoformatting
```bash
make fmt
```

## Package Publishing

* Download the [Aptos CLI](https://aptos.dev/tools/aptos-cli/install-cli/).
* Set the environment variable `APTOS_CLI_PATH` to the full path of the CLI.
* `poetry run python -m aptos_sdk.cli` and set the appropriate command-line parameters

## Generating types
The Python `openapi-python-client` tool cannot parse references. Therefore there are three options:

- Use swagger-cli to dereference, gain a type explosion, and still have missing types
- Live without missing types
- Write a pure python implementation with no autogenerated code

Currently the team is moving forward with pure python, but leaves the following notes for the curious:

```bash
npm install -g @apidevtools/swagger-cli
swagger-cli bundle --dereference ../../../api/doc/v0/openapi.yaml -t yaml > openapi.yaml
python3 -m openapi_python_client generate --path openapi.yaml
mv aptos-dev-api-specification-client/aptos_dev_api_specification_client/ aptos_sdk/openapi
```

## Semantic versioning
This project follows [semver](https://semver.org/) as closely as possible

[repo]: https://github.com/aptos-labs/aptos-core
[pypi-image-version]: https://img.shields.io/pypi/v/aptos-sdk.svg
[pypi-image-downloads]: https://img.shields.io/pypi/dm/aptos-sdk.svg
[pypi-url]: https://pypi.org/project/aptos-sdk
[discord-image]: https://img.shields.io/discord/945856774056083548?label=Discord&logo=discord&style=flat~~~~
[discord-url]: https://discord.gg/aptosnetwork
2 changes: 2 additions & 0 deletions aptos_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright © Aptos Foundation
# SPDX-License-Identifier: Apache-2.0
178 changes: 178 additions & 0 deletions aptos_sdk/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Copyright © Aptos Foundation
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

import json
import tempfile
import unittest

from . import asymmetric_crypto, asymmetric_crypto_wrapper, ed25519, secp256k1_ecdsa
from .account_address import AccountAddress
from .authenticator import AccountAuthenticator
from .bcs import Serializer
from .transactions import RawTransactionInternal


class Account:
"""Represents an account as well as the private, public key-pair for the Aptos blockchain."""

account_address: AccountAddress
private_key: asymmetric_crypto.PrivateKey

def __init__(
self, account_address: AccountAddress, private_key: asymmetric_crypto.PrivateKey
):
self.account_address = account_address
self.private_key = private_key

def __eq__(self, other: object) -> bool:
if not isinstance(other, Account):
return NotImplemented
return (
self.account_address == other.account_address
and self.private_key == other.private_key
)

@staticmethod
def generate() -> Account:
private_key = ed25519.PrivateKey.random()
account_address = AccountAddress.from_key(private_key.public_key())
return Account(account_address, private_key)

@staticmethod
def generate_secp256k1_ecdsa() -> Account:
private_key = secp256k1_ecdsa.PrivateKey.random()
public_key = asymmetric_crypto_wrapper.PublicKey(private_key.public_key())
account_address = AccountAddress.from_key(public_key)
return Account(account_address, private_key)

@staticmethod
def load_key(key: str) -> Account:
private_key = ed25519.PrivateKey.from_str(key)
account_address = AccountAddress.from_key(private_key.public_key())
return Account(account_address, private_key)

@staticmethod
def load(path: str) -> Account:
with open(path) as file:
data = json.load(file)
return Account(
AccountAddress.from_str_relaxed(data["account_address"]),
ed25519.PrivateKey.from_str(data["private_key"]),
)

def store(self, path: str):
data = {
"account_address": str(self.account_address),
"private_key": str(self.private_key),
}
with open(path, "w") as file:
json.dump(data, file)

def address(self) -> AccountAddress:
"""Returns the address associated with the given account"""

return self.account_address

def auth_key(self) -> str:
"""Returns the auth_key for the associated account"""
return str(AccountAddress.from_key(self.private_key.public_key()))

def sign(self, data: bytes) -> asymmetric_crypto.Signature:
return self.private_key.sign(data)

def sign_simulated_transaction(
self, transaction: RawTransactionInternal
) -> AccountAuthenticator:
return transaction.sign_simulated(self.private_key.public_key())

def sign_transaction(
self, transaction: RawTransactionInternal
) -> AccountAuthenticator:
return transaction.sign(self.private_key)

def public_key(self) -> asymmetric_crypto.PublicKey:
"""Returns the public key for the associated account"""

return self.private_key.public_key()


class RotationProofChallenge:
type_info_account_address: AccountAddress = AccountAddress.from_str("0x1")
type_info_module_name: str = "account"
type_info_struct_name: str = "RotationProofChallenge"
sequence_number: int
originator: AccountAddress
current_auth_key: AccountAddress
new_public_key: asymmetric_crypto.PublicKey

def __init__(
self,
sequence_number: int,
originator: AccountAddress,
current_auth_key: AccountAddress,
new_public_key: asymmetric_crypto.PublicKey,
):
self.sequence_number = sequence_number
self.originator = originator
self.current_auth_key = current_auth_key
self.new_public_key = new_public_key

def serialize(self, serializer: Serializer):
self.type_info_account_address.serialize(serializer)
serializer.str(self.type_info_module_name)
serializer.str(self.type_info_struct_name)
serializer.u64(self.sequence_number)
self.originator.serialize(serializer)
self.current_auth_key.serialize(serializer)
serializer.struct(self.new_public_key)


class Test(unittest.TestCase):
def test_load_and_store(self):
(file, path) = tempfile.mkstemp()
start = Account.generate()
start.store(path)
load = Account.load(path)

self.assertEqual(start, load)
# Auth key and Account address should be the same at start
self.assertEqual(str(start.address()), start.auth_key())

def test_key(self):
message = b"test message"
account = Account.generate()
signature = account.sign(message)
self.assertTrue(account.public_key().verify(message, signature))

def test_rotation_proof_challenge(self):
# Create originating account from private key.
originating_account = Account.load_key(
"005120c5882b0d492b3d2dc60a8a4510ec2051825413878453137305ba2d644b"
)
# Create target account from private key.
target_account = Account.load_key(
"19d409c191b1787d5b832d780316b83f6ee219677fafbd4c0f69fee12fdcdcee"
)
# Construct rotation proof challenge.
rotation_proof_challenge = RotationProofChallenge(
sequence_number=1234,
originator=originating_account.address(),
current_auth_key=originating_account.address(),
new_public_key=target_account.public_key(),
)
# Serialize transaction.
serializer = Serializer()
rotation_proof_challenge.serialize(serializer)
rotation_proof_challenge_bcs = serializer.output().hex()
# Compare against expected bytes.
expected_bytes = (
"0000000000000000000000000000000000000000000000000000000000000001"
"076163636f756e7416526f746174696f6e50726f6f664368616c6c656e6765d2"
"0400000000000015b67a673979c7c5dfc8d9c9f94d02da35062a19dd9d218087"
"bd9076589219c615b67a673979c7c5dfc8d9c9f94d02da35062a19dd9d218087"
"bd9076589219c620a1f942a3c46e2a4cd9552c0f95d529f8e3b60bcd44408637"
"9ace35e4458b9f22"
)
self.assertEqual(rotation_proof_challenge_bcs, expected_bytes)
Loading

0 comments on commit f256f1a

Please sign in to comment.