Skip to content

Commit

Permalink
feat: task send transaction(s) (#326)
Browse files Browse the repository at this point in the history
* feat: send transaction task

* test: snapshot tests

* docs: regen docs

* docs: adding command docs

* fix: fixing decoding of dictified txns in sign method to support AppCall txns

* chore: addressing pr comments
  • Loading branch information
aorumbayev authored Oct 12, 2023
1 parent 9bb14aa commit ec60c63
Show file tree
Hide file tree
Showing 24 changed files with 768 additions and 198 deletions.
49 changes: 41 additions & 8 deletions docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,23 +100,28 @@
- [status](#status)
- [stop](#stop)
- [task](#task)
- [sign](#sign)
- [send](#send)
- [Options](#options-17)
- [-a, --account ](#-a---account-)
- [-f, --file ](#-f---file--1)
- [-t, --transaction ](#-t---transaction-)
- [-n, --network ](#-n---network-)
- [sign](#sign)
- [Options](#options-18)
- [-a, --account ](#-a---account-)
- [-f, --file ](#-f---file--2)
- [-t, --transaction ](#-t---transaction--1)
- [-o, --output ](#-o---output--2)
- [--force](#--force-1)
- [transfer](#transfer)
- [Options](#options-18)
- [Options](#options-19)
- [-s, --sender ](#-s---sender-)
- [-r, --receiver ](#-r---receiver--1)
- [--asset, --id ](#--asset---id-)
- [-a, --amount ](#-a---amount--1)
- [--whole-units](#--whole-units-2)
- [-n, --network ](#-n---network-)
- [-n, --network ](#-n---network--1)
- [vanity-address](#vanity-address)
- [Options](#options-19)
- [Options](#options-20)
- [-m, --match ](#-m---match-)
- [-o, --output ](#-o---output--3)
- [-a, --alias ](#-a---alias-)
Expand All @@ -125,19 +130,19 @@
- [Arguments](#arguments-5)
- [KEYWORD](#keyword)
- [wallet](#wallet)
- [Options](#options-20)
- [Options](#options-21)
- [-a, --address ](#-a---address-)
- [-m, --mnemonic](#-m---mnemonic)
- [-f, --force](#-f---force-1)
- [Arguments](#arguments-6)
- [ALIAS_NAME](#alias_name)
- [Arguments](#arguments-7)
- [ALIAS](#alias)
- [Options](#options-21)
- [Options](#options-22)
- [-f, --force](#-f---force-2)
- [Arguments](#arguments-8)
- [ALIAS](#alias-1)
- [Options](#options-22)
- [Options](#options-23)
- [-f, --force](#-f---force-3)

# algokit
Expand Down Expand Up @@ -688,6 +693,34 @@ Collection of useful tasks to help you develop on Algorand.
algokit task [OPTIONS] COMMAND [ARGS]...
```

### send

Send a signed transaction to the given network.

```shell
algokit task send [OPTIONS]
```

### Options


### -f, --file <file>
Single or multiple message pack encoded signed transactions from binary file to send. Option is mutually exclusive with transaction.


### -t, --transaction <transaction>
Base64 encoded signed transaction to send. Option is mutually exclusive with file.


### -n, --network <network>
Network to use. Refers to localnet by default.


* **Options**

localnet | testnet | mainnet


### sign

Sign goal clerk compatible Algorand transaction(s).
Expand Down
2 changes: 1 addition & 1 deletion docs/features/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ AlgoKit Tasks are a collection of handy tasks that can be used to perform variou
- [Transfer Assets or Algos](./tasks/transfer.md) - Transfer Algos or Assets from one account to another with the AlgoKit Transfer feature. This feature allows you to transfer Algos or Assets from one account to another on Algorand blockchain.
- Opt-in or opt-out of Algorand Assets - Coming soon!
- [Signing transactions](./tasks/sign.md) - Sign goal clerk compatible Algorand transactions.
- Sending transactions - Coming soon!
- [Sending transactions](./tasks/send.md) - Send signed goal clerk compatible Algorand transactions.
- NFD lookups - Coming soon!
- IPFS uploads - Coming soon!
- ARC19 asset minting - Coming soon!
56 changes: 56 additions & 0 deletions docs/features/tasks/send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# AlgoKit Task Send

The AlgoKit Send feature allows you to send signed Algorand transaction(s) to a specified network using the AlgoKit CLI. This feature supports sending single or multiple transactions, either provided directly as a base64 encoded string or from a binary file.

## Usage

Available commands and possible usage as follows:

```bash
$ ~ algokit task send
Usage: algokit task send [OPTIONS]

Send a signed transaction to the given network.

Options:
-f, --file FILE Single or multiple message pack encoded signed transactions from binary file to
send. Option is mutually exclusive with transaction.
-t, --transaction TEXT Base64 encoded signed transaction to send. Option is mutually exclusive with file.
-n, --network [localnet|testnet|mainnet]
Network to use. Refers to `localnet` by default.
-h, --help Show this message and exit.
```

## Options

- `--file, -f PATH`: Specifies the path to a binary file containing single or multiple message pack encoded signed transactions to send. Mutually exclusive with `--transaction` option.
- `--transaction, -t TEXT`: Specifies a single base64 encoded signed transaction to send. Mutually exclusive with `--file` option.
- `--network, -n [localnet|testnet|mainnet]`: Specifies the network to which the transactions will be sent. Refers to `localnet` by default.

> Please note, `--transaction` flag only supports sending a single transaction. If you want to send multiple transactions, you can use the `--file` flag to specify a binary file containing multiple transactions.
## Example

To send a transaction, you can use the `send` command as follows:

```bash
$ algokit task send --file {PATH_TO_BINARY_FILE_CONTAINING_SIGNED_TRANSACTIONS}
```

This will send the transactions to the default `localnet` network. If you want to send the transactions to a different network, you can use the `--network` flag:

```bash
$ algokit task send --transaction {YOUR_BASE64_ENCODED_SIGNED_TRANSACTION} --network testnet
```

You can also pipe the `stdout` of `algokit sign` command:

```bash
$ algokit task sign --account {YOUR_ACCOUNT_ALIAS OR YOUR_ADDRESS} --file {PATH_TO_BINARY_FILE_CONTAINING_TRANSACTIONS} --force | algokit task send --network {network_name}
```

If the transaction is successfully sent, the transaction ID (txid) will be output to the console. You can check the transaction status at the provided transaction explorer URL.

## Goal Compatibility

Please note, at the moment this feature only supports [`goal clerk`](https://developer.algorand.org/docs/clis/goal/clerk/clerk/) compatible transaction objects.
2 changes: 1 addition & 1 deletion docs/features/tasks/sign.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ When `--output` option is not specified, the signed transaction(s) will be outpu

```
[
{txn_id: "TRANSACTION_ID", content: "BASE64_ENCODED_SIGNED_TRANSACTION"},
{transaction_id: "TRANSACTION_ID", content: "BASE64_ENCODED_SIGNED_TRANSACTION"},
]
```

Expand Down
2 changes: 2 additions & 0 deletions src/algokit/cli/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import click

from algokit.cli.tasks.send_transaction import send
from algokit.cli.tasks.sign_transaction import sign
from algokit.cli.tasks.transfer import transfer
from algokit.cli.tasks.vanity_address import vanity_address
Expand All @@ -19,3 +20,4 @@ def task_group() -> None:
task_group.add_command(vanity_address)
task_group.add_command(transfer)
task_group.add_command(sign)
task_group.add_command(send)
166 changes: 166 additions & 0 deletions src/algokit/cli/tasks/send_transaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import json
import logging
from io import TextIOWrapper
from pathlib import Path
from typing import cast

import click
from algosdk import encoding, error
from algosdk.transaction import SignedTransaction, retrieve_from_file

from algokit.cli.common.utils import MutuallyExclusiveOption
from algokit.cli.tasks.utils import get_transaction_explorer_url, load_algod_client, stdin_has_content

logger = logging.getLogger(__name__)


def _is_sign_task_output_txn(item: dict) -> bool:
"""
Checks if a given item is a dictionary and contains the keys "transaction_id" and "content".
Args:
item (dict): A dictionary object to be checked.
Returns:
bool: True if the input item is a dictionary with the keys "transaction_id" and "content", False otherwise.
"""

return isinstance(item, dict) and all(key in item for key in ["transaction_id", "content"])


def _load_from_stdin() -> list[SignedTransaction]:
"""
Load transaction data from standard input and convert it into a list of SignedTransaction objects.
Returns:
A list of SignedTransaction objects representing the loaded transactions from the standard input.
Raises:
click.ClickException: If the piped transaction content is invalid.
"""
# Read the raw file content from the standard input

raw_file_content = cast(TextIOWrapper, click.get_text_stream("stdin")).read()

try:
# Parse the raw file content as JSON
file_content = json.loads(raw_file_content)
except json.JSONDecodeError as ex:
raise click.ClickException("Invalid piped transaction content!") from ex

# Check if the content is a list of dicts with the required fields
if not isinstance(file_content, list) or not all(_is_sign_task_output_txn(item) for item in file_content):
raise click.ClickException("Invalid piped transaction content!")

# Convert the content into SignedTransaction objects
return [encoding.msgpack_decode(item["content"]) for item in file_content] # type: ignore[no-untyped-call]


def _get_signed_transactions(file: Path | None = None, transaction: str | None = None) -> list[SignedTransaction]:
"""
Retrieves a list of signed transactions.
Args:
file (Optional[Path]): A `Path` object representing the file path from which to retrieve the transactions.
transaction (Optional[str]): A base64 encoded string representing a single signed transaction.
Returns:
list[SignedTransaction]: A list of `SignedTransaction` objects representing the retrieved signed transactions.
Raises:
click.ClickException: If the supplied transaction is not of type `SignedTransaction`.
click.ClickException: If there is an error decoding the transaction.
"""
try:
if file:
txns = retrieve_from_file(str(file)) # type: ignore[no-untyped-call]
elif transaction:
txns = [encoding.msgpack_decode(transaction)] # type: ignore[no-untyped-call]
else:
txns = _load_from_stdin()

for txn in txns:
if not isinstance(txn, SignedTransaction):
raise click.ClickException("Supplied transaction is not signed!")

return cast(list[SignedTransaction], txns)

except Exception as ex:
logger.debug(ex, exc_info=True)
raise click.ClickException(
"Failed to decode transaction! If you are intending to send multiple transactions use `--file` instead."
) from ex


def _send_transactions(network: str, txns: list[SignedTransaction]) -> None:
"""
Sends a list of signed transactions to the Algorand blockchain network using the AlgodClient.
Args:
network (str): The network to which the transactions will be sent.
txns (list[SignedTransaction]): A list of signed transactions to be sent.
Returns:
None: The function does not return any value.
"""
algod_client = load_algod_client(network)

if any(txn.transaction.group for txn in txns):
txid = algod_client.send_transactions(txns)
click.echo(f"Transaction group successfully sent with txid: {txid}")
click.echo(f"Check transaction group status at: {get_transaction_explorer_url(txid, network)}")
else:
for index, txn in enumerate(txns, start=1):
click.echo(f"\nSending transaction {index}/{len(txns)}")
txid = algod_client.send_transaction(txn)
click.echo(f"Transaction successfully sent with txid: {txid}")
click.echo(f"Check transaction status at: {get_transaction_explorer_url(txid, network)}")


@click.command(name="send", help="Send a signed transaction to the given network.")
@click.option(
"--file",
"-f",
type=click.Path(exists=True, dir_okay=False, file_okay=True, resolve_path=True, path_type=Path),
help="Single or multiple message pack encoded signed transactions from binary file to send.",
cls=MutuallyExclusiveOption,
not_required_if=["transaction"],
required=False,
)
@click.option(
"--transaction",
"-t",
type=click.STRING,
help="Base64 encoded signed transaction to send.",
cls=MutuallyExclusiveOption,
not_required_if=["file"],
required=False,
)
@click.option(
"-n",
"--network",
type=click.Choice(["localnet", "testnet", "mainnet"]),
default="localnet",
required=False,
help="Network to use. Refers to `localnet` by default.",
)
def send(*, file: Path | None, transaction: str | None, network: str) -> None:
if not file and not transaction and not stdin_has_content():
raise click.ClickException(
"Please provide a file path via `--file` or a base64 encoded signed transaction via `--transaction`. "
"Alternatively, you can also pipe the output of `algokit task sign` to this command."
)

txns = _get_signed_transactions(file, transaction)

if not txns:
raise click.ClickException("No valid transactions found!")

try:
_send_transactions(network, txns)
except error.AlgodHTTPError as ex:
raise click.ClickException(str(ex)) from ex
except Exception as ex:
logger.debug(ex, exc_info=True)
raise click.ClickException("Failed to send transaction!") from ex
Loading

0 comments on commit ec60c63

Please sign in to comment.