Skip to content

Commit f256f1a

Browse files
committed
[SDK] Initial commit copying from Aptos-core
1 parent bcd6ac3 commit f256f1a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+9308
-2
lines changed

.flake8

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[flake8]
2+
max-line-length = 88
3+
select = C,E,F,W,B,B9
4+
ignore = E203, E501, W503
5+
exclude = __init__.py

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Other stuff
2+
*.egg.info
3+
/myvenv
4+
15
# Byte-compiled / optimized / DLL files
26
__pycache__/
37
*.py[cod]
@@ -158,3 +162,4 @@ cython_debug/
158162
# and can be added to the global gitignore or merged into this file. For a more nuclear
159163
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160164
#.idea/
165+

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Aptos Python SDK Changelog
2+
3+
All notable changes to the Aptos Python SDK will be captured in this file. This changelog is written by hand for now.
4+
5+
## 0.8.0
6+
- Add support for SingleKeyAuthenicatoin component of AIP-55
7+
- Add support for Secp256k1 Ecdsa of AIP-49
8+
- Add support for Sponsored transactions of AIP-39 and AIP-53
9+
- Improved support for MultiEd25519
10+
11+
## 0.7.0
12+
- **[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`.
13+
- **[Breaking Change]**: Rewrote the large package publisher to support large modules too
14+
- **[Breaking Change]**: Delete sync client
15+
- **[Breaking Change]**: Removed the `hex` function from `AccountAddress`. Instead of `addr.hex()` use `str(addr)`.
16+
- **[Breaking Change]**: The string representation of `AccountAddress` now conforms to [AIP-40](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md).
17+
- **[Breaking Change]**: `AccountAddress.from_hex` and `PrivateKey.from_hex` have been renamed to `from_str`.
18+
- Port remaining sync examples to async (hello-blockchain, multisig, your-coin)
19+
- Updated token client to use events to acquire minted tokens
20+
- Update many dependencies and set Python 3.8.1 as the minimum requirement
21+
- Add support for an experimental chunked uploader
22+
- Add experimental support for the Aptos CLI enabling local end-to-end testing, package building, and package integration tests
23+
24+
## 0.6.4
25+
- Change sync client library from httpX to requests due to latency concerns.
26+
27+
## 0.6.2
28+
- Added custom header "x-aptos-client" to both sync/async RestClient
29+
30+
## 0.6.1
31+
- Updated package manifest.
32+
33+
## 0.6.0
34+
- Add token client.
35+
- Add support for generating account addresses.
36+
- Add support for http2
37+
- Add async client
38+

CONTRIBUTING.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Contributing Guide
2+
## Publishing
3+
To publish the SDK, follow these steps.
4+
5+
First, make sure you have updated the changelog and bumped the SDK version if necessary.
6+
7+
Configure Poetry with the PyPi credentials:
8+
9+
```
10+
poetry config pypi-token.pypi <token>
11+
```
12+
13+
You can get the token from our credential management system, search for PyPi.
14+
15+
Build and publish:
16+
```
17+
poetry publish --build
18+
```

Makefile

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright © Aptos Foundation
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
test:
5+
poetry run python -m unittest discover -s aptos_sdk/ -p '*.py' -t ..
6+
7+
test-coverage:
8+
poetry run python -m coverage run -m unittest discover -s aptos_sdk/ -p '*.py' -t ..
9+
poetry run python -m coverage report
10+
11+
fmt:
12+
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
13+
poetry run isort aptos_sdk examples
14+
poetry run black aptos_sdk examples
15+
16+
lint:
17+
poetry run mypy aptos_sdk examples
18+
poetry run flake8 aptos_sdk examples
19+
20+
examples:
21+
poetry run python -m examples.aptos_token
22+
poetry run python -m examples.read_aggregator
23+
poetry run python -m examples.simple_nft
24+
poetry run python -m examples.simple_aptos_token
25+
poetry run python -m examples.simulate_transfer_coin
26+
poetry run python -m examples.transfer_coin
27+
poetry run python -m examples.transfer_two_by_two
28+
29+
examples_cli:
30+
poetry run python -m unittest -b examples.integration_test
31+
32+
.PHONY: examples fmt lint test

README.md

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,89 @@
1-
# aptos-python-sdk
2-
Aptos Python SDK
1+
# Aptos Python SDK
2+
[![Discord][discord-image]][discord-url]
3+
[![PyPI Package Version][pypi-image-version]][pypi-url]
4+
[![PyPI Package Downloads][pypi-image-downloads]][pypi-url]
5+
6+
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).
7+
8+
Currently this is still in development and may not be suitable for production purposes.
9+
10+
Note: The sync client is deprecated, please only start new projects using the async client. Feature contributions to the sync client will be rejected.
11+
12+
## Requirements
13+
This SDK uses [Poetry](https://python-poetry.org/docs/#installation) for packaging and dependency management:
14+
15+
```
16+
curl -sSL https://install.python-poetry.org | python3 -
17+
poetry install
18+
```
19+
20+
## Unit testing
21+
```bash
22+
make test
23+
```
24+
25+
## E2E testing and Using the Aptos CLI
26+
27+
* Download the [Aptos CLI](https://aptos.dev/tools/aptos-cli/install-cli/).
28+
* Set the environment variable `APTOS_CLI_PATH` to the full path of the CLI.
29+
* `make examples_cli`
30+
31+
We of course allow you to do this a bit more manually by:
32+
33+
First, run a local testnet (run this from the root of aptos-core):
34+
35+
```bash
36+
cargo run -p aptos -- node run-local-testnet --force-restart --assume-yes
37+
```
38+
39+
Next, tell the end-to-end tests to talk to this locally running testnet:
40+
41+
```bash
42+
export APTOS_NODE_URL="http://127.0.0.1:8080/v1"
43+
export APTOS_FAUCET_URL="http://127.0.0.1:8081"
44+
```
45+
46+
Finally run the tests:
47+
48+
```bash
49+
make examples
50+
```
51+
52+
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.
53+
54+
## Autoformatting
55+
```bash
56+
make fmt
57+
```
58+
59+
## Package Publishing
60+
61+
* Download the [Aptos CLI](https://aptos.dev/tools/aptos-cli/install-cli/).
62+
* Set the environment variable `APTOS_CLI_PATH` to the full path of the CLI.
63+
* `poetry run python -m aptos_sdk.cli` and set the appropriate command-line parameters
64+
65+
## Generating types
66+
The Python `openapi-python-client` tool cannot parse references. Therefore there are three options:
67+
68+
- Use swagger-cli to dereference, gain a type explosion, and still have missing types
69+
- Live without missing types
70+
- Write a pure python implementation with no autogenerated code
71+
72+
Currently the team is moving forward with pure python, but leaves the following notes for the curious:
73+
74+
```bash
75+
npm install -g @apidevtools/swagger-cli
76+
swagger-cli bundle --dereference ../../../api/doc/v0/openapi.yaml -t yaml > openapi.yaml
77+
python3 -m openapi_python_client generate --path openapi.yaml
78+
mv aptos-dev-api-specification-client/aptos_dev_api_specification_client/ aptos_sdk/openapi
79+
```
80+
81+
## Semantic versioning
82+
This project follows [semver](https://semver.org/) as closely as possible
83+
84+
[repo]: https://github.com/aptos-labs/aptos-core
85+
[pypi-image-version]: https://img.shields.io/pypi/v/aptos-sdk.svg
86+
[pypi-image-downloads]: https://img.shields.io/pypi/dm/aptos-sdk.svg
87+
[pypi-url]: https://pypi.org/project/aptos-sdk
88+
[discord-image]: https://img.shields.io/discord/945856774056083548?label=Discord&logo=discord&style=flat~~~~
89+
[discord-url]: https://discord.gg/aptosnetwork

aptos_sdk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright © Aptos Foundation
2+
# SPDX-License-Identifier: Apache-2.0

aptos_sdk/account.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Copyright © Aptos Foundation
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import tempfile
8+
import unittest
9+
10+
from . import asymmetric_crypto, asymmetric_crypto_wrapper, ed25519, secp256k1_ecdsa
11+
from .account_address import AccountAddress
12+
from .authenticator import AccountAuthenticator
13+
from .bcs import Serializer
14+
from .transactions import RawTransactionInternal
15+
16+
17+
class Account:
18+
"""Represents an account as well as the private, public key-pair for the Aptos blockchain."""
19+
20+
account_address: AccountAddress
21+
private_key: asymmetric_crypto.PrivateKey
22+
23+
def __init__(
24+
self, account_address: AccountAddress, private_key: asymmetric_crypto.PrivateKey
25+
):
26+
self.account_address = account_address
27+
self.private_key = private_key
28+
29+
def __eq__(self, other: object) -> bool:
30+
if not isinstance(other, Account):
31+
return NotImplemented
32+
return (
33+
self.account_address == other.account_address
34+
and self.private_key == other.private_key
35+
)
36+
37+
@staticmethod
38+
def generate() -> Account:
39+
private_key = ed25519.PrivateKey.random()
40+
account_address = AccountAddress.from_key(private_key.public_key())
41+
return Account(account_address, private_key)
42+
43+
@staticmethod
44+
def generate_secp256k1_ecdsa() -> Account:
45+
private_key = secp256k1_ecdsa.PrivateKey.random()
46+
public_key = asymmetric_crypto_wrapper.PublicKey(private_key.public_key())
47+
account_address = AccountAddress.from_key(public_key)
48+
return Account(account_address, private_key)
49+
50+
@staticmethod
51+
def load_key(key: str) -> Account:
52+
private_key = ed25519.PrivateKey.from_str(key)
53+
account_address = AccountAddress.from_key(private_key.public_key())
54+
return Account(account_address, private_key)
55+
56+
@staticmethod
57+
def load(path: str) -> Account:
58+
with open(path) as file:
59+
data = json.load(file)
60+
return Account(
61+
AccountAddress.from_str_relaxed(data["account_address"]),
62+
ed25519.PrivateKey.from_str(data["private_key"]),
63+
)
64+
65+
def store(self, path: str):
66+
data = {
67+
"account_address": str(self.account_address),
68+
"private_key": str(self.private_key),
69+
}
70+
with open(path, "w") as file:
71+
json.dump(data, file)
72+
73+
def address(self) -> AccountAddress:
74+
"""Returns the address associated with the given account"""
75+
76+
return self.account_address
77+
78+
def auth_key(self) -> str:
79+
"""Returns the auth_key for the associated account"""
80+
return str(AccountAddress.from_key(self.private_key.public_key()))
81+
82+
def sign(self, data: bytes) -> asymmetric_crypto.Signature:
83+
return self.private_key.sign(data)
84+
85+
def sign_simulated_transaction(
86+
self, transaction: RawTransactionInternal
87+
) -> AccountAuthenticator:
88+
return transaction.sign_simulated(self.private_key.public_key())
89+
90+
def sign_transaction(
91+
self, transaction: RawTransactionInternal
92+
) -> AccountAuthenticator:
93+
return transaction.sign(self.private_key)
94+
95+
def public_key(self) -> asymmetric_crypto.PublicKey:
96+
"""Returns the public key for the associated account"""
97+
98+
return self.private_key.public_key()
99+
100+
101+
class RotationProofChallenge:
102+
type_info_account_address: AccountAddress = AccountAddress.from_str("0x1")
103+
type_info_module_name: str = "account"
104+
type_info_struct_name: str = "RotationProofChallenge"
105+
sequence_number: int
106+
originator: AccountAddress
107+
current_auth_key: AccountAddress
108+
new_public_key: asymmetric_crypto.PublicKey
109+
110+
def __init__(
111+
self,
112+
sequence_number: int,
113+
originator: AccountAddress,
114+
current_auth_key: AccountAddress,
115+
new_public_key: asymmetric_crypto.PublicKey,
116+
):
117+
self.sequence_number = sequence_number
118+
self.originator = originator
119+
self.current_auth_key = current_auth_key
120+
self.new_public_key = new_public_key
121+
122+
def serialize(self, serializer: Serializer):
123+
self.type_info_account_address.serialize(serializer)
124+
serializer.str(self.type_info_module_name)
125+
serializer.str(self.type_info_struct_name)
126+
serializer.u64(self.sequence_number)
127+
self.originator.serialize(serializer)
128+
self.current_auth_key.serialize(serializer)
129+
serializer.struct(self.new_public_key)
130+
131+
132+
class Test(unittest.TestCase):
133+
def test_load_and_store(self):
134+
(file, path) = tempfile.mkstemp()
135+
start = Account.generate()
136+
start.store(path)
137+
load = Account.load(path)
138+
139+
self.assertEqual(start, load)
140+
# Auth key and Account address should be the same at start
141+
self.assertEqual(str(start.address()), start.auth_key())
142+
143+
def test_key(self):
144+
message = b"test message"
145+
account = Account.generate()
146+
signature = account.sign(message)
147+
self.assertTrue(account.public_key().verify(message, signature))
148+
149+
def test_rotation_proof_challenge(self):
150+
# Create originating account from private key.
151+
originating_account = Account.load_key(
152+
"005120c5882b0d492b3d2dc60a8a4510ec2051825413878453137305ba2d644b"
153+
)
154+
# Create target account from private key.
155+
target_account = Account.load_key(
156+
"19d409c191b1787d5b832d780316b83f6ee219677fafbd4c0f69fee12fdcdcee"
157+
)
158+
# Construct rotation proof challenge.
159+
rotation_proof_challenge = RotationProofChallenge(
160+
sequence_number=1234,
161+
originator=originating_account.address(),
162+
current_auth_key=originating_account.address(),
163+
new_public_key=target_account.public_key(),
164+
)
165+
# Serialize transaction.
166+
serializer = Serializer()
167+
rotation_proof_challenge.serialize(serializer)
168+
rotation_proof_challenge_bcs = serializer.output().hex()
169+
# Compare against expected bytes.
170+
expected_bytes = (
171+
"0000000000000000000000000000000000000000000000000000000000000001"
172+
"076163636f756e7416526f746174696f6e50726f6f664368616c6c656e6765d2"
173+
"0400000000000015b67a673979c7c5dfc8d9c9f94d02da35062a19dd9d218087"
174+
"bd9076589219c615b67a673979c7c5dfc8d9c9f94d02da35062a19dd9d218087"
175+
"bd9076589219c620a1f942a3c46e2a4cd9552c0f95d529f8e3b60bcd44408637"
176+
"9ace35e4458b9f22"
177+
)
178+
self.assertEqual(rotation_proof_challenge_bcs, expected_bytes)

0 commit comments

Comments
 (0)