diff --git a/docs/python_SDK_guide.md b/docs/python_SDK_guide.md index 4cccf98..b4c33e2 100644 --- a/docs/python_SDK_guide.md +++ b/docs/python_SDK_guide.md @@ -1,66 +1,74 @@ -## Overview +## Overview -This reference documents all the methods available in the SDK, and explains in detail how these methods work. -SDKs are open source, and you can use them according to the licence. +This reference documents all the methods available in the SDK, and explains in detail how these methods work. SDKs are +open source, and you can use them according to the licence. The library client specifications can be found here: [](./api_docs/client.md) - ## Getting Started ### Installing + ```sh pip install flow-py-sdk ``` -or +or ```sh poetry add flow-py-sdk ``` ### Importing the Library + ```sh import flow-py-sdk ``` -## Running examples +## Running examples See [Running examples](./examples.md) ## Connect + [](./api_docs/client.md#create-a-flow-client) -The library uses gRPC to communicate with the access nodes and it must be configured with correct access node API URL. +The library uses gRPC to communicate with the access nodes and it must be configured with correct access node API URL. + +📖 **Access API URLs** can be found [here](https://docs.onflow.org/access-api/#flow-access-node-endpoints). An error +will be returned if the host is unreachable. The Access Nodes APIs hosted by DapperLabs are accessible at: -📖 **Access API URLs** can be found [here](https://docs.onflow.org/access-api/#flow-access-node-endpoints). An error will be returned if the host is unreachable. -The Access Nodes APIs hosted by DapperLabs are accessible at: - Testnet `access.devnet.nodes.onflow.org:9000` - Mainnet `access.mainnet.nodes.onflow.org:9000` -- Local Emulator `127.0.0.1:3569` +- Local Emulator `127.0.0.1:3569` Example: ```py async with flow_client( - host="127.0.0.1", port="3569" - ) as flow_client: - # do something with `flow_client` + host="127.0.0.1", port="3569" +) as flow_client: +# do something with `flow_client` ``` ## Querying the Flow Network -After you have established a connection with an access node, you can query the Flow network to retrieve data about blocks, accounts, events and transactions. We will explore how to retrieve each of these entities in the sections below. + +After you have established a connection with an access node, you can query the Flow network to retrieve data about +blocks, accounts, events and transactions. We will explore how to retrieve each of these entities in the sections below. ### Get Blocks + [](./api_docs/client.md#query-blocks) Query the network for block by id, height or get the latest block. -📖 **Block ID** is SHA3-256 hash of the entire block payload. This hash is stored as an ID field on any block response object (ie. response from `GetLatestBlock`). +📖 **Block ID** is SHA3-256 hash of the entire block payload. This hash is stored as an ID field on any block response +object (ie. response from `GetLatestBlock`). -📖 **Block height** expresses the height of the block on the chain. The latest block height increases by one for every valid block produced. +📖 **Block height** expresses the height of the block on the chain. The latest block height increases by one for every +valid block produced. #### Examples @@ -73,44 +81,48 @@ You can use the `GetLatestBlock` method to fetch the latest sealed or unsealed b ```python async with flow_client( host=ctx.access_node_host, port=ctx.access_node_port - ) as client: - block = await client.get_latest_block( - is_sealed = False - # or is_sealed = True can be used for retrieving sealed block - ) - print("Block ID: {}".format(block.id.hex())) - print("Block height: {}".format(block.height)) - print("Block timestamp: [{}]".format(block.timestamp)) +) as client: + block = await client.get_latest_block( + is_sealed=False + # or is_sealed = True can be used for retrieving sealed block + ) + print("Block ID: {}".format(block.id.hex())) + print("Block height: {}".format(block.height)) + print("Block timestamp: [{}]".format(block.timestamp)) ``` + You can use the `get_block_by_i_d` method to fetch the specific block with desired ID: ```python async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: latest_block = await client.get_latest_block() block = await client.get_block_by_height( - id = latest_block.id + id=latest_block.id ) print("Block ID: {}".format(block.id.hex())) print("Block height: {}".format(block.height)) print("Block timestamp: [{}]".format(block.timestamp)) ``` + Also `get_block_by_height` method can be used to fetch the specific block with desired height: ```python async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: latest_block = await client.get_latest_block() block = await client.get_block_by_height( - height = latest_block.height + height=latest_block.height ) print("Block ID: {}".format(block.id.hex())) print("Block height: {}".format(block.height)) print("Block timestamp: [{}]".format(block.timestamp)) ``` + Result output: + ```bash Block ID: 8d08c88873d079d8f2d929853a647a8703597898532f3b7f79b0e3b0320d0bf7 Block height: 146 @@ -126,28 +138,33 @@ Block timestamp: [2021-10-28 14:12:41.172587+00:00] ``` ### Get Account + [](./api_docs/client.md#accounts) Retrieve any account from Flow network's latest block or from a specified block height. -📖 **Account address** is a unique account identifier. Be mindful about the `0x` prefix, you should use the prefix as a default representation but be careful and safely handle user inputs without the prefix. +📖 **Account address** is a unique account identifier. Be mindful about the `0x` prefix, you should use the prefix as a +default representation but be careful and safely handle user inputs without the prefix. An account includes the following data: + - Address: the account address. - Balance: balance of the account. - Contracts: list of contracts deployed to the account. - Keys: list of keys associated with the account. #### Examples + Example depicts ways to get an account at the latest block and at a specific block height: **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/account_examples.py)** Get an account using its address. + ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: account = await client.get_account( address=ctx.service_account_address.bytes @@ -157,11 +174,13 @@ async def run(self, ctx: Config): print("Account Contracts: {}".format(len(account.contracts))) print("Account Keys: {}".format(len(account.keys))) ``` + Get an account by address at the given block height. + ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: latest_block = await client.get_latest_block() _, _, _ = await random_account(client=client, ctx=ctx) @@ -174,11 +193,13 @@ async def run(self, ctx: Config): print("Account Contracts: {}".format(len(account.contracts))) print("Account Keys: {}".format(len(account.keys))) ``` + Get an account by address at the latest sealed block. + ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: _, _, _ = await random_account(client=client, ctx=ctx) account = await client.get_account_at_latest_block( @@ -191,6 +212,7 @@ async def run(self, ctx: Config): ``` Result output: + ```bash Account Address: f8d6e0586b0a20c7 Account Balance: 999999999999700000 @@ -203,13 +225,15 @@ Account Balance: 2 Account Keys: 1 ``` - ### Get Transactions + [](./api_docs/client.md#accounts) -Retrieve transactions from the network by providing a transaction ID. After a transaction has been submitted, you can also get the transaction result to check the status. +Retrieve transactions from the network by providing a transaction ID. After a transaction has been submitted, you can +also get the transaction result to check the status. -📖 **Transaction ID** is a hash of the encoded transaction payload and can be calculated before submitting the transaction to the network. +📖 **Transaction ID** is a hash of the encoded transaction payload and can be calculated before submitting the +transaction to the network. ⚠️ The transaction ID provided must be from the current spork. @@ -224,15 +248,13 @@ Retrieve transactions from the network by providing a transaction ID. After a tr | SEALED | ✅ | The transaction has been executed and the result is sealed in a block | | EXPIRED | ✅ | The transaction reference block is outdated before being executed | - **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/transactions_examples.py)** ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: - account_address, _, new_signer = await random_account( client=client, ctx=ctx ) @@ -271,7 +293,9 @@ async def run(self, ctx: Config): print("transaction script: {}".format(transaction.script.decode("utf-8"))) ``` + Example output: + ```bash transaction ID: 8f8adbfbe85cee39d3ee180a8c148b8ebc7bca8feae9f64a4c6f0c65e9db6663 transaction payer: 01cf0e2f2f715450 @@ -279,33 +303,37 @@ transaction proposer: 01cf0e2f2f715450 transaction script: transaction(){prepare(){log("OK")}} ``` - ### Get Events + [](./api_docs/client.md#events) Retrieve events by a given type in a specified block height range or through a list of block IDs. 📖 **Event type** is a string that follow a standard format: + ``` A.{contract address}.{contract name}.{event name} ``` -Please read more about [events in the documentation](https://docs.onflow.org/core-contracts/flow-token/). The exception to this standard are -core events, and you should read more about them in [this document](https://docs.onflow.org/cadence/language/core-events/). +Please read more about [events in the documentation](https://docs.onflow.org/core-contracts/flow-token/). The exception +to this standard are core events, and you should read more about them +in [this document](https://docs.onflow.org/cadence/language/core-events/). 📖 **Block height range** expresses the height of the start and end block in the chain. #### Examples + Example depicts ways to get events within block range or by block IDs: **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/events_examples.py)** -This example shows how to retrieve events by name in the block height range Class. -In this example, an account is created and then we try to get "AccountCreated" event. +This example shows how to retrieve events by name in the block height range Class. In this example, an account is +created and then we try to get "AccountCreated" event. + ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: _, _, _ = await random_account(client=client, ctx=ctx) latest_block = await client.get_latest_block() @@ -316,12 +344,13 @@ async def run(self, ctx: Config): ) ``` -This example shows how to retrieve events by name in the block ids Function -In this example, an account is created and then we try to get "AccountCreated" event. +This example shows how to retrieve events by name in the block ids Function In this example, an account is created and +then we try to get "AccountCreated" event. + ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: _, _, _ = await random_account(client=client, ctx=ctx) latest_block = await client.get_latest_block() @@ -332,10 +361,11 @@ async def run(self, ctx: Config): print("event value: {}".format(events[0].events[0].value)) print("event value: {}".format(events[0].events[0].transaction_id.hex())) ``` + ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: address, _, _ = await random_account( client=client, @@ -396,7 +426,9 @@ async def run(self, ctx: Config): print("event value: {}".format(result.events[0].transaction_id.hex())) ``` + Example output: + ```bash event type: flow.AccountCreated event value: flow.AccountCreated(address: 0xe9dd1081676bbc90) @@ -408,10 +440,12 @@ event value: dfc8c1ea51279ddc74c16ed7644361dbe4828181d56497a4ebb18a6bbf0fd574 ``` ### Get Collections + [](./api_docs/client.md#collections) -Retrieve a batch of transactions that have been included in the same block, known as ***collections***. -Collections are used to improve consensus throughput by increasing the number of transactions per block and they act as a link between a block and a transaction. +Retrieve a batch of transactions that have been included in the same block, known as ***collections***. Collections are +used to improve consensus throughput by increasing the number of transactions per block and they act as a link between a +block and a transaction. 📖 **Collection ID** is SHA3-256 hash of the collection payload. @@ -422,7 +456,7 @@ Collections are used to improve consensus throughput by increasing the number of ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: _, _, _ = await random_account(client=client, ctx=ctx) block = await client.get_latest_block(is_sealed=True) @@ -436,24 +470,32 @@ async def run(self, ctx: Config): ) ) ``` + Example output: + ```bash ID: afc6a69bf375f0eee80635091d50a1c4bb5479c1e6a04803e422a06614c45a7c Transactions: [d3a6b0cb53dfc72c38f365bb177a327c2bae8d4a6076a2909fc11d8f95510396] ``` ### Execute Scripts + [](./api_docs/client.md#scripts) -Scripts allow you to write arbitrary non-mutating Cadence code on the Flow blockchain and return data. You can learn more about [Cadence and scripts here](https://docs.onflow.org/cadence/language/), but we are now only interested in executing the script code and getting back the data. +Scripts allow you to write arbitrary non-mutating Cadence code on the Flow blockchain and return data. You can learn +more about [Cadence and scripts here](https://docs.onflow.org/cadence/language/), but we are now only interested in +executing the script code and getting back the data. -We can execute a script using the latest state of the Flow blockchain or we can choose to execute the script at a specific time in history defined by a block height or block ID. +We can execute a script using the latest state of the Flow blockchain or we can choose to execute the script at a +specific time in history defined by a block height or block ID. -📖 **Block ID** is SHA3-256 hash of the entire block payload, but you can get that value from the block response properties. +📖 **Block ID** is SHA3-256 hash of the entire block payload, but you can get that value from the block response +properties. 📖 **Block height** expresses the height of the block in the chain. **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/scripts_examples.py)** + ``` // simple script pub fun main(a: Int): Int { @@ -495,7 +537,7 @@ async def run(self, ctx: Config): """) async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: await client.execute_script( script=script @@ -503,7 +545,9 @@ async def run(self, ctx: Config): # , block_height ) ``` + or it can be a more complex function which has some input: + ```python async def run(self, ctx: Config): script = Script( @@ -532,7 +576,7 @@ async def run(self, ctx: Config): ) async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: complex_script = await client.execute_script( script=script @@ -544,6 +588,7 @@ async def run(self, ctx: Config): print("Balance: {}".format(complex_script.fields[0].value)) ``` + ```bash Name: flow Address: 0000000000000001 @@ -551,19 +596,29 @@ Balance: 1000000000 ``` ## Mutate Flow Network -Flow, like most blockchains, allows anybody to submit a transaction that mutates the shared global chain state. A transaction is an object that holds a payload, which describes the state mutation, and one or more authorizations that permit the transaction to mutate the state owned by specific accounts. -Transaction data is composed and signed with help of the SDK. The signed payload of transaction then gets submitted to the access node API. If a transaction is invalid or the correct number of authorizing signatures are not provided, it gets rejected. +Flow, like most blockchains, allows anybody to submit a transaction that mutates the shared global chain state. A +transaction is an object that holds a payload, which describes the state mutation, and one or more authorizations that +permit the transaction to mutate the state owned by specific accounts. + +Transaction data is composed and signed with help of the SDK. The signed payload of transaction then gets submitted to +the access node API. If a transaction is invalid or the correct number of authorizing signatures are not provided, it +gets rejected. Executing a transaction requires couple of steps: + - [Building a transaction](#build-transactions). - [Signing a transaction](#sign-transactions). - [Sending a transaction](#send-transactions). ## Transactions -A transaction is nothing more than a signed set of data that includes script code which are instructions on how to mutate the network state and properties that define and limit it's execution. All these properties are explained bellow. -📖 **Script** field is the portion of the transaction that describes the state mutation logic. On Flow, transaction logic is written in [Cadence](https://docs.onflow.org/cadence/). Here is an example transaction script: +A transaction is nothing more than a signed set of data that includes script code which are instructions on how to +mutate the network state and properties that define and limit it's execution. All these properties are explained bellow. + +📖 **Script** field is the portion of the transaction that describes the state mutation logic. On Flow, transaction +logic is written in [Cadence](https://docs.onflow.org/cadence/). Here is an example transaction script: + ``` transaction(greeting: String) { execute { @@ -572,47 +627,64 @@ transaction(greeting: String) { } ``` -📖 **Arguments**. A transaction can accept zero or more arguments that are passed into the Cadence script. The arguments on the transaction must match the number and order declared in the Cadence script. Sample script from above accepts a single `String` argument. +📖 **Arguments**. A transaction can accept zero or more arguments that are passed into the Cadence script. The arguments +on the transaction must match the number and order declared in the Cadence script. Sample script from above accepts a +single `String` argument. -📖 **[Proposal key](https://docs.onflow.org/concepts/transaction-signing/#proposal-key)** must be provided to act as a sequence number and prevent reply and other potential attacks. +📖 **[Proposal key](https://docs.onflow.org/concepts/transaction-signing/#proposal-key)** must be provided to act as a +sequence number and prevent reply and other potential attacks. -Each account key maintains a separate transaction sequence counter; the key that lends its sequence number to a transaction is called the proposal key. +Each account key maintains a separate transaction sequence counter; the key that lends its sequence number to a +transaction is called the proposal key. A proposal key contains three fields: + - Account address - Key index - Sequence number -A transaction is only valid if its declared sequence number matches the current on-chain sequence number for that key. The sequence number increments by one after the transaction is executed. +A transaction is only valid if its declared sequence number matches the current on-chain sequence number for that key. +The sequence number increments by one after the transaction is executed. -📖 **[Payer](https://docs.onflow.org/concepts/transaction-signing/#signer-roles)** is the account that pays the fees for the transaction. A transaction must specify exactly one payer. The payer is only responsible for paying the network and gas fees; the transaction is not authorized to access resources or code stored in the payer account. +📖 **[Payer](https://docs.onflow.org/concepts/transaction-signing/#signer-roles)** is the account that pays the fees for +the transaction. A transaction must specify exactly one payer. The payer is only responsible for paying the network and +gas fees; the transaction is not authorized to access resources or code stored in the payer account. -📖 **[Authorizers](https://docs.onflow.org/concepts/transaction-signing/#signer-roles)** are accounts that authorize a transaction to read and mutate their resources. A transaction can specify zero or more authorizers, depending on how many accounts the transaction needs to access. +📖 **[Authorizers](https://docs.onflow.org/concepts/transaction-signing/#signer-roles)** are accounts that authorize a +transaction to read and mutate their resources. A transaction can specify zero or more authorizers, depending on how +many accounts the transaction needs to access. -The number of authorizers on the transaction must match the number of AuthAccount parameters declared in the prepare statement of the Cadence script. +The number of authorizers on the transaction must match the number of AuthAccount parameters declared in the prepare +statement of the Cadence script. Example transaction with multiple authorizers: + ``` transaction { prepare(authorizer1: AuthAccount, authorizer2: AuthAccount) { } } ``` -📖 **Gas limit** is the limit on the amount of computation a transaction requires, and it will abort if it exceeds its gas limit. -Cadence uses metering to measure the number of operations per transaction. You can read more about it in the [Cadence documentation](/cadence). +📖 **Gas limit** is the limit on the amount of computation a transaction requires, and it will abort if it exceeds its +gas limit. Cadence uses metering to measure the number of operations per transaction. You can read more about it in +the [Cadence documentation](/cadence). -The gas limit depends on the complexity of the transaction script. Until dedicated gas estimation tooling exists, it's best to use the emulator to test complex transactions and determine a safe limit. +The gas limit depends on the complexity of the transaction script. Until dedicated gas estimation tooling exists, it's +best to use the emulator to test complex transactions and determine a safe limit. -📖 **Reference block** specifies an expiration window (measured in blocks) during which a transaction is considered valid by the network. -A transaction will be rejected if it is submitted past its expiry block. Flow calculates transaction expiry using the _reference block_ field on a transaction. -A transaction expires after `600` blocks are committed on top of the reference block, which takes about 10 minutes at average Mainnet block rates. +📖 **Reference block** specifies an expiration window (measured in blocks) during which a transaction is considered +valid by the network. A transaction will be rejected if it is submitted past its expiry block. Flow calculates +transaction expiry using the _reference block_ field on a transaction. A transaction expires after `600` blocks are +committed on top of the reference block, which takes about 10 minutes at average Mainnet block rates. ### Build Transactions + [](./api_docs/client.md#transactions) -Building a transaction involves setting the required properties explained above and producing a transaction object. +Building a transaction involves setting the required properties explained above and producing a transaction object. -Here we define a simple transaction script that will be used to execute on the network and serve as a good learning example. +Here we define a simple transaction script that will be used to execute on the network and serve as a good learning +example. ``` transaction(greeting: String) { @@ -629,7 +701,8 @@ transaction(greeting: String) { } ``` -**[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/transactions_examples.py)** +**[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/transactions_examples.py)** + ```python transaction = Tx( code="""transaction(){prepare(){log("OK")}}""", @@ -645,18 +718,23 @@ transaction = Tx( After you have successfully [built a transaction](#build-transactions) the next step in the process is to sign it. ### Sign Transactions + [](./api_docs/signer.md) -Flow introduces new concepts that allow for more flexibility when creating and signing transactions. -Before trying the examples below, we recommend that you read through the [transaction signature documentation](https://docs.onflow.org/concepts/accounts-and-keys/). +Flow introduces new concepts that allow for more flexibility when creating and signing transactions. Before trying the +examples below, we recommend that you read through +the [transaction signature documentation](https://docs.onflow.org/concepts/accounts-and-keys/). -After you have successfully [built a transaction](#build-transactions) the next step in the process is to sign it. Flow transactions have envelope and payload signatures, and you should learn about each in the [signature documentation](https://docs.onflow.org/concepts/accounts-and-keys/#anatomy-of-a-transaction). +After you have successfully [built a transaction](#build-transactions) the next step in the process is to sign it. Flow +transactions have envelope and payload signatures, and you should learn about each in +the [signature documentation](https://docs.onflow.org/concepts/accounts-and-keys/#anatomy-of-a-transaction). Quick example of building a transaction: + ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: account_address, _, new_signer = await random_account( client=client, ctx=ctx @@ -678,15 +756,18 @@ async def run(self, ctx: Config): ) ``` -Signatures can be generated more securely using keys stored in a hardware device such as an [HSM](https://en.wikipedia.org/wiki/Hardware_security_module). The `crypto.Signer` interface is intended to be flexible enough to support a variety of signer implementations and is not limited to in-memory implementations. +Signatures can be generated more securely using keys stored in a hardware device such as +an [HSM](https://en.wikipedia.org/wiki/Hardware_security_module). The `crypto.Signer` interface is intended to be +flexible enough to support a variety of signer implementations and is not limited to in-memory implementations. Simple signature example: + ```python async def run(self, ctx: Config): # First Step : Create a client to connect to the flow blockchain # flow_client function creates a client using the host and port async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: account_address, _, new_signer = await random_account( client=client, ctx=ctx @@ -712,7 +793,8 @@ async def run(self, ctx: Config): ) ``` -Flow supports great flexibility when it comes to transaction signing, we can define multiple authorizers (multi-sig transactions) and have different payer account than proposer. We will explore advanced signing scenarios bellow. +Flow supports great flexibility when it comes to transaction signing, we can define multiple authorizers (multi-sig +transactions) and have different payer account than proposer. We will explore advanced signing scenarios bellow. ### [Single party, single signature](https://docs.onflow.org/concepts/transaction-signing/#single-party-single-signature) @@ -725,31 +807,32 @@ Flow supports great flexibility when it comes to transaction signing, we can def | `0x01` | 1 | 1.0 | **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/transactions_examples.py)** + ```python async def run(self, ctx: Config): address = Address.from_hex("0x01") - account = await client.get_account(address = address) + account = await client.get_account(address=address) # Assume you stored private key somewhere safe and restore it in private_key. - signer = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key.to_string().hex()) + signer = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key.to_string().hex()) async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: latest_block = await client.get_latest_block() - + tx = Tx( code=""" transaction { prepare(signer: AuthAccount) { log(signer.address) } } """, - reference_block_id = latest_block.id, - payer = account.address, - proposal_key = ProposalKey( - key_address = account.address, - key_id = 0, - key_sequence_number = account.keys[ + reference_block_id=latest_block.id, + payer=account.address, + proposal_key=ProposalKey( + key_address=account.address, + key_id=0, + key_sequence_number=account.keys[ 0 ].sequence_number, ), @@ -761,7 +844,6 @@ async def run(self, ctx: Config): ) ``` - ### [Single party, multiple signatures](https://docs.onflow.org/concepts/transaction-signing/#single-party-multiple-signatures) - Proposer, payer and authorizer are the same account (`0x01`). @@ -774,34 +856,35 @@ async def run(self, ctx: Config): | `0x01` | 2 | 0.5 | **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/transactions_examples.py)** + ```python async def run(self, ctx: Config): address = Address.from_hex("0x01") - account = await client.get_account(address = address) + account = await client.get_account(address=address) # Assume you stored private key somewhere safe and restore it in private_key. - signer1 = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key1.to_string().hex()) - signer2 = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key2.to_string().hex()) + signer1 = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key1.to_string().hex()) + signer2 = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key2.to_string().hex()) async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: latest_block = await client.get_latest_block() - + tx = Tx( code=""" transaction { prepare(signer: AuthAccount) { log(signer.address) } } """, - reference_block_id = latest_block.id, - payer = account.address, - proposal_key = ProposalKey( - key_address = account.address, - key_id = 0, - key_sequence_number = account.keys[ + reference_block_id=latest_block.id, + payer=account.address, + proposal_key=ProposalKey( + key_address=account.address, + key_id=0, + key_sequence_number=account.keys[ 0 ].sequence_number, ), @@ -831,37 +914,38 @@ async def run(self, ctx: Config): | `0x02` | 3 | 1.0 | **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/trasnactions_examples.py)** + ```python async def run(self, ctx: Config): # First Step : Create a client to connect to the flow blockchain # flow_client function creates a client using the host and port address1 = Address.from_hex("0x01") address3 = Address.from_hex("0x02") - account = await client.get_account(address = address) + account = await client.get_account(address=address) # Assume you stored private key somewhere safe and restore it in private_key. - signer1 = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key1.to_string().hex()) - signer3 = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key3.to_string().hex()) + signer1 = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key1.to_string().hex()) + signer3 = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key3.to_string().hex()) async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: latest_block = await client.get_latest_block() - + tx = Tx( code=""" transaction { prepare(signer: AuthAccount) { log(signer.address) } } """, - reference_block_id = latest_block.id, - payer = account3.address, - proposal_key = ProposalKey( - key_address = account1.address, - key_id = 0, - key_sequence_number = account.keys[ + reference_block_id=latest_block.id, + payer=account3.address, + proposal_key=ProposalKey( + key_address=account1.address, + key_id=0, + key_sequence_number=account.keys[ 0 ].sequence_number, ), @@ -892,37 +976,38 @@ async def run(self, ctx: Config): | `0x02` | 3 | 1.0 | **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/transactions_examples.py)** + ```python async def run(self, ctx: Config): # First Step : Create a client to connect to the flow blockchain # flow_client function creates a client using the host and port address1 = Address.from_hex("0x01") address3 = Address.from_hex("0x02") - account = await client.get_account(address = address) + account = await client.get_account(address=address) # Assume you stored private key somewhere safe and restore it in private_key. - signer1 = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key1.to_string().hex()) - signer3 = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key3.to_string().hex()) + signer1 = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key1.to_string().hex()) + signer3 = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key3.to_string().hex()) async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: latest_block = await client.get_latest_block() - + tx = Tx( code=""" transaction { prepare(signer: AuthAccount) { log(signer.address) } } """, - reference_block_id = latest_block.id, - payer = account3.address, - proposal_key = ProposalKey( - key_address = account1.address, - key_id = 0, - key_sequence_number = account.keys[ + reference_block_id=latest_block.id, + payer=account3.address, + proposal_key=ProposalKey( + key_address=account1.address, + key_id=0, + key_sequence_number=account.keys[ 0 ].sequence_number, ), @@ -955,43 +1040,44 @@ async def run(self, ctx: Config): | `0x02` | 4 | 0.5 | **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/transactions_examples.py)** + ```python async def run(self, ctx: Config): # First Step : Create a client to connect to the flow blockchain # flow_client function creates a client using the host and port address1 = Address.from_hex("0x01") address3 = Address.from_hex("0x02") - account = await client.get_account(address = address) + account = await client.get_account(address=address) # Assume you stored private key somewhere safe and restore it in private_key. - signer1 = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key1.to_string().hex()) - signer2 = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key2.to_string().hex()) - signer3 = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key3.to_string().hex()) - signer4 = in_memory_signer.InMemorySigner(hash_algo = hash_algo, - sign_algo = sign_algo, - private_key_hex = private_key4.to_string().hex()) + signer1 = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key1.to_string().hex()) + signer2 = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key2.to_string().hex()) + signer3 = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key3.to_string().hex()) + signer4 = in_memory_signer.InMemorySigner(hash_algo=hash_algo, + sign_algo=sign_algo, + private_key_hex=private_key4.to_string().hex()) async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: latest_block = await client.get_latest_block() - + tx = Tx( code=""" transaction { prepare(signer: AuthAccount) { log(signer.address) } } """, - reference_block_id = latest_block.id, - payer = account2.address, - proposal_key = ProposalKey( - key_address = account1.address, - key_id = 0, - key_sequence_number = account.keys[ + reference_block_id=latest_block.id, + payer=account2.address, + proposal_key=ProposalKey( + key_address=account1.address, + key_id=0, + key_sequence_number=account.keys[ 0 ].sequence_number, ), @@ -1015,19 +1101,59 @@ async def run(self, ctx: Config): ) ``` +### Signing user messages + +Signing and verifying user messages can be done by using `Signer.sign_user_message`. Verifying that an account (via its +owners keys) has been signed can be done with `utils.verify_user_signature` + +**[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/user_message_examples.py)** + +Short sample: + +```python +async def run(self, ctx: Config): + async with flow_client( + host=ctx.access_node_host, port=ctx.access_node_port + ) as client: + # create account + account_address, _, account_signer = await random_account( + client=client, + ctx=ctx, + ) + + # the message to sign. Could include some extra information, like the reference block id or the address. + message = b"Hello World!" + + # sign message + signature = account_signer.sign_user_message(message) + c_signature = utils.CompositeSignature( + account_address.hex(), 0, signature.hex() + ) + + # verify the signature is valid + signature_is_valid = await utils.verify_user_signature( + message=message, + client=client, + composite_signatures=[c_signature], + ) + assert signature_is_valid +``` ### Send Transactions -[](./api_docs/client.md#transactions) -After a transaction has been [built](#build-transactions) and [signed](#sign-transactions), it can be sent to the Flow blockchain where it will be executed. If sending was successful you can then [retrieve the transaction result](#get-transactions). +[](./api_docs/client.md#transactions) +After a transaction has been [built](#build-transactions) and [signed](#sign-transactions), it can be sent to the Flow +blockchain where it will be executed. If sending was successful you can +then [retrieve the transaction result](#get-transactions). **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/transactions_examples.py)** + ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: account_address, _, new_signer = await random_account( client=client, ctx=ctx @@ -1056,52 +1182,63 @@ async def run(self, ctx: Config): ``` ### Create Accounts + **[](https://github.com/janezpodhostnik/flow-py-sdk/blob/master/examples/account_examples.py)** -On Flow, account creation happens inside a transaction. Because the network allows for a many-to-many relationship between public keys and accounts, it's not possible to derive a new account address from a public key offline. +On Flow, account creation happens inside a transaction. Because the network allows for a many-to-many relationship +between public keys and accounts, it's not possible to derive a new account address from a public key offline. -The Flow VM uses a deterministic address generation algorithm to assigen account addresses on chain. You can find more details about address generation in the [accounts & keys documentation](https://docs.onflow.org/concepts/accounts-and-keys/). +The Flow VM uses a deterministic address generation algorithm to assigen account addresses on chain. You can find more +details about address generation in +the [accounts & keys documentation](https://docs.onflow.org/concepts/accounts-and-keys/). #### Public Key -Flow uses ECDSA key pairs to control access to user accounts. Each key pair can be used in combination with the SHA2-256 or SHA3-256 hashing algorithms. + +Flow uses ECDSA key pairs to control access to user accounts. Each key pair can be used in combination with the SHA2-256 +or SHA3-256 hashing algorithms. ⚠️ You'll need to authorize at least one public key to control your new account. -Flow represents ECDSA public keys in raw form without additional metadata. Each key is a single byte slice containing a concatenation of its X and Y components in big-endian byte form. +Flow represents ECDSA public keys in raw form without additional metadata. Each key is a single byte slice containing a +concatenation of its X and Y components in big-endian byte form. -A Flow account can contain zero (not possible to control) or more public keys, referred to as account keys. Read more about [accounts in the documentation](https://docs.onflow.org/concepts/accounts-and-keys/#accounts). +A Flow account can contain zero (not possible to control) or more public keys, referred to as account keys. Read more +about [accounts in the documentation](https://docs.onflow.org/concepts/accounts-and-keys/#accounts). An account key contains the following data: + - Raw public key (described above) - Signature algorithm - Hash algorithm - Weight (integer between 0-1000) -Account creation happens inside a transaction, which means that somebody must pay to submit that transaction to the network. We'll call this person the account creator. Make sure you have read [sending a transaction section](#send-transactions) first. +Account creation happens inside a transaction, which means that somebody must pay to submit that transaction to the +network. We'll call this person the account creator. Make sure you have +read [sending a transaction section](#send-transactions) first. ```python async def run(self, ctx: Config): async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: latest_block = await client.get_latest_block() - proposer = await client.get_account_at_latest_block(address = ctx.service_account_address.bytes) + proposer = await client.get_account_at_latest_block(address=ctx.service_account_address.bytes) tx = ( create_account_template( - keys = [account_key], - reference_block_id = latest_block.id, - payer = ctx.service_account_address, - proposal_key = ProposalKey( - key_address = ctx.service_account_address, - key_id = ctx.service_account_key_id, - key_sequence_number = proposer.keys[ + keys=[account_key], + reference_block_id=latest_block.id, + payer=ctx.service_account_address, + proposal_key=ProposalKey( + key_address=ctx.service_account_address, + key_id=ctx.service_account_key_id, + key_sequence_number=proposer.keys[ ctx.service_account_key_id ].sequence_number, ), ) - .add_authorizers(ctx.service_account_address) - .with_envelope_signature( + .add_authorizers(ctx.service_account_address) + .with_envelope_signature( ctx.service_account_address, 0, ctx.service_account_signer ) ) @@ -1112,17 +1249,19 @@ async def run(self, ctx: Config): print("\nCreating account : successfully done...") ``` -After the account creation transaction has been submitted you can retrieve the new account address by [getting the transaction result](#get-transactions). +After the account creation transaction has been submitted you can retrieve the new account address +by [getting the transaction result](#get-transactions). The new account address will be emitted in a system-level `flow.AccountCreated` event. await client.get_events_for_block_i_ds( - type="flow.AccountCreated", block_ids=[latest_block.id] +type="flow.AccountCreated", block_ids=[latest_block.id] ) ### Contracts -Flow smart contracts are Codance scripts that run on Flow blockchain and can returns values. -a contract can be add, update or remove from an account. + +Flow smart contracts are Codance scripts that run on Flow blockchain and can returns values. a contract can be add, +update or remove from an account. A contracts contains the following fields: @@ -1137,31 +1276,31 @@ async def run(self, ctx: Config): # flow_client function creates a client using the host and port # A test Contract define for this example, you can modify it by your self contract = { - "Name" : "TestOne", - "source" : '''pub contract TestOne { + "Name": "TestOne", + "source": '''pub contract TestOne { pub fun add(a: Int, b: Int): Int { return a + b } }''' - } - contract_source_hex = bytes(contract["source"],"UTF-8").hex() + } + contract_source_hex = bytes(contract["source"], "UTF-8").hex() async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: - account_address, account_key, new_signer = await random_account( client = client, ctx = ctx) + account_address, account_key, new_signer = await random_account(client=client, ctx=ctx) latest_block = await client.get_latest_block() cadenceName = cadence.String(contract["Name"]) cadenceCode = cadence.String(contract_source_hex) tx = ( Tx( - code = addAccountContractTemplate, - reference_block_id = latest_block.id, - payer = account_address, + code=addAccountContractTemplate, + reference_block_id=latest_block.id, + payer=account_address, ).add_arguments(cadenceName) - .add_arguments(cadenceCode) - .add_authorizers([account_address]) - .with_envelope_signature( + .add_arguments(cadenceCode) + .add_authorizers([account_address]) + .with_envelope_signature( account_address, 0, new_signer, @@ -1171,38 +1310,40 @@ async def run(self, ctx: Config): result = await client.execute_transaction(tx) ``` + ## Updating a contract of an account + ```python async def run(self, ctx: Config): # First Step : Create a client to connect to the flow blockchain # flow_client function creates a client using the host and port # A test Contract define for this example, you can modify it by your self contract = { - "Name" : "TestOne", - "source" : '''pub contract TestOne { + "Name": "TestOne", + "source": '''pub contract TestOne { pub fun add(a: Int, b: Int): Int { return a + b } }''' - } - contract_source_hex = bytes(contract["source"],"UTF-8").hex() + } + contract_source_hex = bytes(contract["source"], "UTF-8").hex() async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: - account_address, account_key, new_signer = await random_account( client = client, ctx = ctx) + account_address, account_key, new_signer = await random_account(client=client, ctx=ctx) latest_block = await client.get_latest_block() cadenceName = cadence.String(contract["Name"]) cadenceCode = cadence.String(contract_source_hex) tx = ( Tx( - code = addAccountContractTemplate, - reference_block_id = latest_block.id, - payer = account_address, + code=addAccountContractTemplate, + reference_block_id=latest_block.id, + payer=account_address, ).add_arguments(cadenceName) - .add_arguments(cadenceCode) - .add_authorizers([account_address]) - .with_envelope_signature( + .add_arguments(cadenceCode) + .add_authorizers([account_address]) + .with_envelope_signature( account_address, 0, new_signer, @@ -1212,26 +1353,26 @@ async def run(self, ctx: Config): result = await client.execute_transaction(tx) latest_block = await client.get_latest_block() - #Updated Contract + # Updated Contract contract = { - "Name" : "TestOne", - "source" : '''pub contract TestOne { + "Name": "TestOne", + "source": '''pub contract TestOne { pub fun add(a: Int, b: Int): Int { return a * b } }''' } - contract_source_hex = bytes(contract["source"],"UTF-8").hex() - #Update account contract with a transaction + contract_source_hex = bytes(contract["source"], "UTF-8").hex() + # Update account contract with a transaction tx = ( Tx( - code = updateAccountContractTemplate, - reference_block_id = latest_block.id, - payer = account_address, + code=updateAccountContractTemplate, + reference_block_id=latest_block.id, + payer=account_address, ).add_arguments(contract["Name"]) - .add_arguments(contract_source_hex) - .add_authorizers([account_address]) - .with_envelope_signature( + .add_arguments(contract_source_hex) + .add_authorizers([account_address]) + .with_envelope_signature( account_address, 0, new_signer, @@ -1243,37 +1384,38 @@ async def run(self, ctx: Config): ``` ## Removing a contract from an account + ```python async def run(self, ctx: Config): # First Step : Create a client to connect to the flow blockchain # flow_client function creates a client using the host and port # A test Contract define for this example, you can modify it by your self contract = { - "Name" : "TestOne", - "source" : '''pub contract TestOne { + "Name": "TestOne", + "source": '''pub contract TestOne { pub fun add(a: Int, b: Int): Int { return a + b } }''' - } - contract_source_hex = bytes(contract["source"],"UTF-8").hex() + } + contract_source_hex = bytes(contract["source"], "UTF-8").hex() async with flow_client( - host=ctx.access_node_host, port=ctx.access_node_port + host=ctx.access_node_host, port=ctx.access_node_port ) as client: - account_address, account_key, new_signer = await random_account( client = client, ctx = ctx) + account_address, account_key, new_signer = await random_account(client=client, ctx=ctx) latest_block = await client.get_latest_block() cadenceName = cadence.String(contract["Name"]) cadenceCode = cadence.String(contract_source_hex) tx = ( Tx( - code = addAccountContractTemplate, - reference_block_id = latest_block.id, - payer = account_address, + code=addAccountContractTemplate, + reference_block_id=latest_block.id, + payer=account_address, ).add_arguments(cadenceName) - .add_arguments(cadenceCode) - .add_authorizers([account_address]) - .with_envelope_signature( + .add_arguments(cadenceCode) + .add_authorizers([account_address]) + .with_envelope_signature( account_address, 0, new_signer, @@ -1288,12 +1430,12 @@ async def run(self, ctx: Config): tx = ( Tx( - code = removeAccountContractTemplate, - reference_block_id = latest_block.id, - payer = account_address, + code=removeAccountContractTemplate, + reference_block_id=latest_block.id, + payer=account_address, ).add_arguments(cadenceName) - .add_authorizers([account_address]) - .with_envelope_signature( + .add_authorizers([account_address]) + .with_envelope_signature( account_address, 0, new_signer, @@ -1303,10 +1445,13 @@ async def run(self, ctx: Config): result = await client.execute_transaction(tx) ``` + ### Generate Keys + [](./api_docs/keys.md#generate-new-keys) -Flow uses [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) signatures to control access to user accounts. Each key pair can be used in combination with the `SHA2-256` or `SHA3-256` hashing algorithms. +Flow uses [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) signatures to control access +to user accounts. Each key pair can be used in combination with the `SHA2-256` or `SHA3-256` hashing algorithms. Here's how to generate an ECDSA private key for the P-256 (secp256r1) curve. @@ -1315,4 +1460,6 @@ private_key = ecdsa.SigningKey.generate(curve="ECDSA_secp256k1") public_key = sk.verifying_key.to_string() ``` -The example above uses an ECDSA key pair on the P-256 (secp256r1) elliptic curve. Flow also supports the secp256k1 curve used by Bitcoin and Ethereum. Read more about [supported algorithms here](https://docs.onflow.org/concepts/accounts-and-keys/#supported-signature--hash-algorithms). \ No newline at end of file +The example above uses an ECDSA key pair on the P-256 (secp256r1) elliptic curve. Flow also supports the secp256k1 curve +used by Bitcoin and Ethereum. Read more +about [supported algorithms here](https://docs.onflow.org/concepts/accounts-and-keys/#supported-signature--hash-algorithms). \ No newline at end of file diff --git a/examples/__init__.py b/examples/__init__.py index 6e04b56..9033707 100644 --- a/examples/__init__.py +++ b/examples/__init__.py @@ -14,6 +14,7 @@ import examples.generate_key import examples.events_examples import examples.transactions_examples +import examples.user_message_examples logging_config = toml.load(Path(__file__).parent.joinpath("./logging.toml")) diff --git a/examples/account_examples.py b/examples/account_examples.py index 78e37a8..55ef800 100644 --- a/examples/account_examples.py +++ b/examples/account_examples.py @@ -4,7 +4,7 @@ ProposalKey, create_account_template, Tx, - ContractTemplates, + TransactionTemplates, cadence, SignAlgo, HashAlgo, @@ -102,7 +102,7 @@ async def run(self, ctx: Config): contract_code = cadence.String(contract_source_hex) transaction = ( Tx( - code=ContractTemplates.addAccountContractTemplate, + code=TransactionTemplates.addAccountContractTemplate, reference_block_id=latest_block.id, payer=account_address, proposal_key=ProposalKey( @@ -163,7 +163,7 @@ async def run(self, ctx: Config): contract_code = cadence.String(contract_source_hex) transaction = ( Tx( - code=ContractTemplates.addAccountContractTemplate, + code=TransactionTemplates.addAccountContractTemplate, reference_block_id=latest_block.id, payer=account_address, proposal_key=ProposalKey( @@ -202,7 +202,7 @@ async def run(self, ctx: Config): # Update account contract with a transaction transaction = ( Tx( - code=ContractTemplates.updateAccountContractTemplate, + code=TransactionTemplates.updateAccountContractTemplate, reference_block_id=latest_block.id, payer=account_address, proposal_key=ProposalKey( @@ -263,7 +263,7 @@ async def run(self, ctx: Config): contract_code = cadence.String(contract_source_hex) transaction = ( Tx( - code=ContractTemplates.addAccountContractTemplate, + code=TransactionTemplates.addAccountContractTemplate, reference_block_id=latest_block.id, payer=account_address, proposal_key=ProposalKey( @@ -292,7 +292,7 @@ async def run(self, ctx: Config): transaction = ( Tx( - code=ContractTemplates.removeAccountContractTemplate, + code=TransactionTemplates.removeAccountContractTemplate, reference_block_id=latest_block.id, payer=account_address, proposal_key=ProposalKey( diff --git a/examples/common/utils.py b/examples/common/utils.py index f4c35fb..43b9c8a 100644 --- a/examples/common/utils.py +++ b/examples/common/utils.py @@ -29,11 +29,70 @@ async def random_account( ctx: Config, contracts: dict[Annotated[str, "name"], Annotated[str, "source"]] = None, ) -> (cadence.Address, AccountKey, Signer): - pub, priv = random_key_pair(SignAlgo.ECDSA_P256) + """ + Generate a random account. + Parameters + ---------- + client: AccessAPI + The client to use to create the account. + ctx: Config + The configuration to use. + contracts: dict[str, str] + The contracts to use for the account. - account_key = AccountKey( - public_key=pub, sign_algo=SignAlgo.ECDSA_P256, hash_algo=HashAlgo.SHA3_256 + Returns + ------- + (cadence.Address, AccountKey, Signer) + The address, account key, and signer for the new account. + + """ + address, keys, signers = await random_account_with_weights( + client=client, + ctx=ctx, + weights=[AccountKey.weight_threshold], + contracts=contracts, ) + return address, keys[0], signers[0] + + +async def random_account_with_weights( + *, + client: AccessAPI, + ctx: Config, + weights: list[int], + contracts: dict[Annotated[str, "name"], Annotated[str, "source"]] = None, +) -> (cadence.Address, list[AccountKey], list[Signer]): + """ + Generate a random account with a given set of weights. + + Parameters + ---------- + client: AccessAPI + The client to use to create the account. + ctx: Config + The configuration to use. + weights: list[int] + The weights to use for the account. + contracts: dict[str, str] + The contracts to use for the account. + + Returns + ------- + (cadence.Address, list[AccountKey], list[Signer]) + The address, account keys, and signers for the new account. + + """ + keys = [random_key_pair(SignAlgo.ECDSA_P256) for _ in weights] + + account_keys = [ + AccountKey( + public_key=keys[i][0], + sign_algo=SignAlgo.ECDSA_P256, + hash_algo=HashAlgo.SHA3_256, + weight=weights[i], + ) + for i in range(len(keys)) + ] block = await client.get_latest_block() proposer = await client.get_account_at_latest_block( @@ -42,7 +101,7 @@ async def random_account( tx = ( create_account_template( - keys=[account_key], + keys=account_keys, contracts=contracts, reference_block_id=block.id, payer=ctx.service_account_address, @@ -69,10 +128,13 @@ async def random_account( return ( new_addresses[0], - account_key, - InMemorySigner( - sign_algo=SignAlgo.ECDSA_P256, - hash_algo=HashAlgo.SHA3_256, - private_key_hex=priv.hex(), - ), + account_keys, + [ + InMemorySigner( + sign_algo=SignAlgo.ECDSA_P256, + hash_algo=HashAlgo.SHA3_256, + private_key_hex=priv.hex(), + ) + for _, priv in keys + ], ) diff --git a/examples/main.py b/examples/main.py index 27cb7a1..fe0b57c 100644 --- a/examples/main.py +++ b/examples/main.py @@ -21,7 +21,12 @@ async def run_async(ctx: Config, examples: list[str]) -> Annotated[bool, "Succes def run(): # last index of string "examples" - example_index = sys.argv.index("examples") + try: + example_index = sys.argv.index("examples") + except ValueError: + # used if run is called without any arguments + # for example when running this file directly without the `poetry run examples` command + example_index = 0 examples = sys.argv[example_index + 1 :] config_location = pathlib.Path(__file__).parent.resolve().joinpath("./flow.json") diff --git a/examples/user_message_examples.py b/examples/user_message_examples.py new file mode 100644 index 0000000..9270d82 --- /dev/null +++ b/examples/user_message_examples.py @@ -0,0 +1,64 @@ +from flow_py_sdk import ( + SignAlgo, + HashAlgo, + InMemorySigner, + InMemoryVerifier, + flow_client, + AccountKey, + utils, +) +from examples.common.utils import random_account, random_account_with_weights +from examples.common import Example, Config + + +# ------------------------------------------------------------------------- +# Sign and verify a user message +# this example shows how to verify a message was signed by the owner(s) of an account +# ------------------------------------------------------------------------- +class SignAndVerifyUserMessageExample(Example): + def __init__(self) -> None: + super().__init__( + tag="V.1.", name="SignAndVerifyUserMessageExample", sort_order=601 + ) + + async def run(self, ctx: Config): + # generate a random account with 3 keys + async with flow_client( + host=ctx.access_node_host, port=ctx.access_node_port + ) as client: + # create account with tree half weight keys + # only two signatures are required to sign a message (or a transaction) + account_address, _, account_signers = await random_account_with_weights( + client=client, + ctx=ctx, + weights=[ + int(AccountKey.weight_threshold / 2), + int(AccountKey.weight_threshold / 2), + int(AccountKey.weight_threshold / 2), + ], + ) + + # the message to sign. Could include some extra information, like the reference block id or the address. + message = b"Hello World!" + + # get two signatures from the account signers + # signer 1 + signature = account_signers[0].sign_user_message(message) + c_signature_1 = utils.CompositeSignature( + account_address.hex(), 0, signature.hex() + ) + + # signer 3 + signature = account_signers[2].sign_user_message(message) + c_signature_2 = utils.CompositeSignature( + account_address.hex(), 2, signature.hex() + ) + + # verify the signature is valid + signature_is_valid = await utils.verify_user_signature( + message=message, + client=client, + composite_signatures=[c_signature_1, c_signature_2], + ) + + assert signature_is_valid diff --git a/flow_py_sdk/__init__.py b/flow_py_sdk/__init__.py index 21baae7..447a1cd 100644 --- a/flow_py_sdk/__init__.py +++ b/flow_py_sdk/__init__.py @@ -3,9 +3,16 @@ from .client import flow_client, AccessAPI, entities from .script import Script from .exceptions import PySDKError, NotCadenceValueError -from .signer import SignAlgo, HashAlgo, InMemorySigner, Signer +from .signer import ( + SignAlgo, + HashAlgo, + InMemorySigner, + InMemoryVerifier, + Signer, + Verifier, +) from .account_key import AccountKey -from .templates import create_account_template, ContractTemplates +from .templates import create_account_template, TransactionTemplates from .tx import Tx, ProposalKey, TxSignature, TransactionStatus logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/flow_py_sdk/signer/__init__.py b/flow_py_sdk/signer/__init__.py index ac167b5..37d10c6 100644 --- a/flow_py_sdk/signer/__init__.py +++ b/flow_py_sdk/signer/__init__.py @@ -1,4 +1,6 @@ from flow_py_sdk.signer.hash_algo import HashAlgo from flow_py_sdk.signer.sign_algo import SignAlgo -from flow_py_sdk.signer.signer import Signer +from flow_py_sdk.signer.signer import Signer, TransactionDomainTag, UserDomainTag +from flow_py_sdk.signer.verifier import Verifier +from flow_py_sdk.signer.in_memory_verifier import InMemoryVerifier from flow_py_sdk.signer.in_memory_signer import InMemorySigner diff --git a/flow_py_sdk/signer/in_memory_signer.py b/flow_py_sdk/signer/in_memory_signer.py index 4406abb..19df40c 100644 --- a/flow_py_sdk/signer/in_memory_signer.py +++ b/flow_py_sdk/signer/in_memory_signer.py @@ -3,13 +3,13 @@ import ecdsa from flow_py_sdk.signer.hash_algo import HashAlgo +from flow_py_sdk.signer.in_memory_verifier import InMemoryVerifier from flow_py_sdk.signer.sign_algo import SignAlgo from flow_py_sdk.signer.signer import Signer +from flow_py_sdk.signer.verifier import Verifier -class InMemorySigner(Signer): - """The InMemorySigner used for signing transaction and messaged given a private key hex.""" - +class InMemorySigner(Signer, Verifier): def __init__( self, *, hash_algo: HashAlgo, sign_algo: SignAlgo, private_key_hex: str ) -> None: @@ -18,12 +18,23 @@ def __init__( self.key = ecdsa.SigningKey.from_string( bytes.fromhex(private_key_hex), curve=sign_algo.get_signing_curve() ) + self.verifier = InMemoryVerifier( + hash_algo=hash_algo, + sign_algo=sign_algo, + public_key_hex=self.key.get_verifying_key().to_string().hex(), + ) def sign(self, message: bytes, tag: Optional[bytes] = None) -> bytes: + hash_ = self._hash_message(message, tag) + return self.key.sign_digest_deterministic(hash_) + + def verify(self, signature: bytes, message: bytes, tag: bytes) -> bool: + return self.verifier.verify(signature, message, tag) + + def _hash_message(self, message: bytes, tag: Optional[bytes] = None) -> bytes: m = self.hash_algo.create_hasher() if tag: m.update(tag + message) else: m.update(message) - hash_ = m.digest() - return self.key.sign_digest_deterministic(hash_) + return m.digest() diff --git a/flow_py_sdk/signer/in_memory_verifier.py b/flow_py_sdk/signer/in_memory_verifier.py new file mode 100644 index 0000000..37b9024 --- /dev/null +++ b/flow_py_sdk/signer/in_memory_verifier.py @@ -0,0 +1,33 @@ +from typing import Optional + +import ecdsa + +from flow_py_sdk.signer.hash_algo import HashAlgo +from flow_py_sdk.signer.sign_algo import SignAlgo +from flow_py_sdk.signer.verifier import Verifier + + +class InMemoryVerifier(Verifier): + def __init__( + self, *, hash_algo: HashAlgo, sign_algo: SignAlgo, public_key_hex: str + ) -> None: + super().__init__() + self.hash_algo = hash_algo + self.key = ecdsa.VerifyingKey.from_string( + bytes.fromhex(public_key_hex), curve=sign_algo.get_signing_curve() + ) + + def verify(self, signature: bytes, message: bytes, tag: bytes) -> bool: + hash_ = self._hash_message(message, tag) + try: + return self.key.verify_digest(signature, hash_) + except ecdsa.keys.BadSignatureError: + return False + + def _hash_message(self, message: bytes, tag: Optional[bytes] = None) -> bytes: + m = self.hash_algo.create_hasher() + if tag: + m.update(tag + message) + else: + m.update(message) + return m.digest() diff --git a/flow_py_sdk/signer/signer.py b/flow_py_sdk/signer/signer.py index ae98b2c..0df9e8e 100644 --- a/flow_py_sdk/signer/signer.py +++ b/flow_py_sdk/signer/signer.py @@ -1,6 +1,23 @@ from abc import ABC, abstractmethod from typing import Optional +from flow_py_sdk.exceptions import PySDKError + +DomainTagLength = 32 + + +def _padded_domain_tag(s: str) -> bytes: + encoded = s.encode("utf-8") + if len(encoded) > DomainTagLength: + raise PySDKError( + f"domain tag {s} cannot be longer than {DomainTagLength} bytes" + ) + return encoded + bytearray(DomainTagLength - len(s)) + + +TransactionDomainTag = _padded_domain_tag("FLOW-V0.0-transaction") +UserDomainTag = _padded_domain_tag("FLOW-V0.0-user") + class Signer(ABC): """The Signer class @@ -13,11 +30,11 @@ def __init__(self) -> None: @abstractmethod def sign(self, message: bytes, tag: Optional[bytes] = None) -> bytes: - """The signe method signs a message with a tag and returns the signature + """The sign method signs a message with a tag and returns the signature Parameters ---------- - message : int + message : bytes The message to sign. tag : str The tag to sign with. @@ -25,7 +42,43 @@ def sign(self, message: bytes, tag: Optional[bytes] = None) -> bytes: Returns ------- bytes - The signe message and tag. + The signed message. """ pass + + def sign_transaction(self, message: bytes) -> bytes: + """The sign_user_message method signs a message with the transaction tag and returns the signature + + Used to sign user messages + + Parameters + ---------- + message : bytes + The message to sign. + + Returns + ------- + bytes + The signed message. + + """ + return self.sign(message, TransactionDomainTag) + + def sign_user_message(self, message: bytes) -> bytes: + """The sign_user_message method signs a message with the user tag and returns the signature + + Used to sign user messages + + Parameters + ---------- + message : int + The message to sign. + + Returns + ------- + bytes + The signed message. + + """ + return self.sign(message, UserDomainTag) diff --git a/flow_py_sdk/signer/verifier.py b/flow_py_sdk/signer/verifier.py new file mode 100644 index 0000000..b3c17e6 --- /dev/null +++ b/flow_py_sdk/signer/verifier.py @@ -0,0 +1,69 @@ +from abc import ABC, abstractmethod + +from flow_py_sdk.signer import TransactionDomainTag, UserDomainTag + + +class Verifier(ABC): + """The Verifier class + + This is an abstract base class that is used for transaction and message signature verification. + """ + + def __init__(self): + super().__init__() + + @abstractmethod + def verify(self, signature: bytes, message: bytes, tag: bytes) -> bool: + """The verify method signs a message with a tag and returns the signature + + Parameters + ---------- + signature : bytes + The signature to verify. + message : bytes + The message to verify. + tag : str + The tag to verify. + + Returns + ------- + bool + Is the signature valid. + + """ + pass + + def verify_transaction(self, signature: bytes, message: bytes) -> bool: + """The verify method signs a message with a transaction domain tag and returns the signature + + Parameters + ---------- + signature : bytes + The signature to verify. + message : bytes + The message to verify. + + Returns + ------- + bool + Is the signature valid. + + """ + return self.verify(signature, message, TransactionDomainTag) + + def verify_user_message(self, signature: bytes, message: bytes) -> bool: + """The verify method signs a message with a user domain tag and returns the signature + + Parameters + ---------- + signature : bytes + The signature to verify. + message : bytes + The message to verify. + Returns + ------- + bool + Is the signature valid. + + """ + return self.verify(signature, message, UserDomainTag) diff --git a/flow_py_sdk/templates.py b/flow_py_sdk/templates.py index 9d3e5d4..8b413b4 100644 --- a/flow_py_sdk/templates.py +++ b/flow_py_sdk/templates.py @@ -57,21 +57,79 @@ def create_account_template( return tx -class ContractTemplates: - updateAccountContractTemplate = """transaction(name: String, code: String) { +class TransactionTemplates: + updateAccountContractTemplate = """ + transaction(name: String, code: String) { prepare(signer: AuthAccount) { signer.contracts.update__experimental(name: name, code: code.decodeHex()) } - }""" + } + """ - addAccountContractTemplate = """transaction(name: String, code: String) { + addAccountContractTemplate = """ + transaction(name: String, code: String) { prepare(signer: AuthAccount) { signer.contracts.add(name: name, code: code.decodeHex()) } - }""" + } + """ - removeAccountContractTemplate = """transaction(name: String) { + removeAccountContractTemplate = """ + transaction(name: String) { prepare(signer: AuthAccount) { signer.contracts.remove(name: name) } - }""" + } + """ + + verifyAccountSignaturesTemplate = """ + import Crypto + pub fun main( + address: Address, + signatures: [String], + keyIndexes: [Int], + message: String, + ): Bool { + let keyList = Crypto.KeyList() + + let account = getAccount(address) + let keys = account.keys + for keyIndex in keyIndexes { + if let key = keys.get(keyIndex: keyIndex) { + if key.isRevoked { + // cannot verify: the key at this index is revoked + return false + } + keyList.add( + PublicKey( + publicKey: key.publicKey.publicKey, + signatureAlgorithm: key.publicKey.signatureAlgorithm + ), + hashAlgorithm: key.hashAlgorithm, + weight: key.weight / 1000.0, + ) + } else { + // cannot verify: they key at this index doesn't exist + return false + } + } + + let signatureSet: [Crypto.KeyListSignature] = [] + + var i = 0 + for signature in signatures { + signatureSet.append( + Crypto.KeyListSignature( + keyIndex: i, + signature: signature.decodeHex() + ) + ) + i = i + 1 + } + + return keyList.verify( + signatureSet: signatureSet, + signedData: message.utf8, + ) + } + """ diff --git a/flow_py_sdk/tx.py b/flow_py_sdk/tx.py index 66589a2..7335f1d 100644 --- a/flow_py_sdk/tx.py +++ b/flow_py_sdk/tx.py @@ -4,7 +4,7 @@ import rlp from flow_py_sdk.cadence import Value, Address, encode_arguments -from flow_py_sdk.exceptions import NotCadenceValueError, PySDKError +from flow_py_sdk.exceptions import NotCadenceValueError from flow_py_sdk.frlp import rlp_encode_uint64 from flow_py_sdk.proto.flow import entities from flow_py_sdk.signer import Signer @@ -27,21 +27,6 @@ class TransactionStatus(Enum): TransactionStatusExpired = 5 -DomainTagLength = 32 - - -def _padded_domain_tag(s: str) -> bytes: - encoded = s.encode("utf-8") - if len(encoded) > DomainTagLength: - raise PySDKError( - f"domain tag {s} cannot be longer than {DomainTagLength} bytes" - ) - return encoded + bytearray(DomainTagLength - len(s)) - - -TransactionDomainTag = _padded_domain_tag("FLOW-V0.0-transaction") - - class TxSignature(object): def __init__( self, address: Address, key_id: int, signer_index: int, signature: bytes @@ -196,13 +181,13 @@ def _submit_signature(self) -> "Tx": f"The transaction needs [{', '.join(self._missing_fields_for_signing())}] before it can be signed" ) for s in self.payload_signers: - signature = s.signer.sign(self.payload_message(), TransactionDomainTag) + signature = s.signer.sign_transaction(self.payload_message()) signer_index = self._signer_list().index(s.address) ts = TxSignature(s.address, s.key_id, signer_index, signature) self.payload_signatures.append(ts) for s in self.envelope_signers: - signature = s.signer.sign(self.envelope_message(), TransactionDomainTag) + signature = s.signer.sign_transaction(self.envelope_message()) signer_index = self._signer_list().index(s.address) ts = TxSignature(s.address, s.key_id, signer_index, signature) self.envelope_signatures.append(ts) diff --git a/flow_py_sdk/utils/__init__.py b/flow_py_sdk/utils/__init__.py new file mode 100644 index 0000000..802030c --- /dev/null +++ b/flow_py_sdk/utils/__init__.py @@ -0,0 +1 @@ +from .verify_user_signature import verify_user_signature, CompositeSignature diff --git a/flow_py_sdk/utils/verify_user_signature.py b/flow_py_sdk/utils/verify_user_signature.py new file mode 100644 index 0000000..fe195ad --- /dev/null +++ b/flow_py_sdk/utils/verify_user_signature.py @@ -0,0 +1,50 @@ +from dataclasses import dataclass + +from flow_py_sdk import cadence +from flow_py_sdk.exceptions import PySDKError +from flow_py_sdk.templates import TransactionTemplates +from flow_py_sdk.client import AccessAPI +from flow_py_sdk.script import Script + + +@dataclass +class CompositeSignature(object): + addr: str + keyId: int + signature: str + + +async def verify_user_signature( + *, client: AccessAPI, message: bytes, composite_signatures: list[CompositeSignature] +) -> bool: + # if there is no signature return False + if len(composite_signatures) == 0: + return False + + # it does not make sense for the signatures to be from different addresses + if any(x.addr != composite_signatures[0].addr for x in composite_signatures): + raise PySDKError("All signatures must be from the same address") + + address = cadence.Address.from_hex(composite_signatures[0].addr) + signatures = cadence.Array( + [cadence.String(x.signature) for x in composite_signatures] + ) + key_indexes = cadence.Array([cadence.Int(x.keyId) for x in composite_signatures]) + cadence_message = cadence.String(str(message, "utf-8")) + + script = Script( + code=TransactionTemplates.verifyAccountSignaturesTemplate, + arguments=[ + address, + signatures, + key_indexes, + cadence_message, + ], + ) + + script_result = await client.execute_script(script) + + if script_result is None: + return False + + return script_result.as_type(cadence.Bool).value diff --git a/tests/signer/test_in_memory_signer.py b/tests/signer/test_in_memory_signer.py index 589af3c..980bd30 100644 --- a/tests/signer/test_in_memory_signer.py +++ b/tests/signer/test_in_memory_signer.py @@ -50,3 +50,47 @@ def test_sign(self): ) self.assertTrue(valid) + + def test_sign_verify(self): + s = [SignAlgo.ECDSA_P256, SignAlgo.ECDSA_secp256k1] + + h = [ + HashAlgo.SHA2_256, + # HashAlgo.SHA2_384, + HashAlgo.SHA3_256, + # HashAlgo.SHA3_384, + ] + + for sign_algo in s: + for hash_algo in h: + with self.subTest(f"sign_algo: {sign_algo}, hash_algo: {hash_algo}"): + private_key = ecdsa.SigningKey.generate( + curve=sign_algo.get_signing_curve() + ) + + signer = InMemorySigner( + sign_algo=sign_algo, + hash_algo=hash_algo, + private_key_hex=private_key.to_string().hex(), + ) + message = b"some_message" + + signature = signer.sign_user_message(message) + valid = signer.verify_user_message(signature, message) + + self.assertTrue(valid) + + signature = signer.sign_transaction(message) + valid = signer.verify_transaction(signature, message) + + self.assertTrue(valid) + + signature = signer.sign_transaction(message) + valid = signer.verify_user_message(signature, message) + + self.assertFalse(valid) + + signature = signer.sign_user_message(message) + valid = signer.verify_transaction(signature, message) + + self.assertFalse(valid)