diff --git a/README.md b/README.md index 44ebe6b..77b0f72 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,6 @@ To add a wallet, you can do the following: ```sh cargo run -- wallet create - ``` This will prompt you for a a name and a password. Keep in mind that losing the @@ -102,8 +101,80 @@ cargo run -- provider create This will prompt you for a name, a kind (only UTxORPC supported), whether it is for mainnet or testnet, a URL and the possibility to add headers. ->>> If you have a [Demeter](https://demeter.run) port you would have to set the URL as `https://{host}` and on put `dmtr-api-key:YOUR_API_KEY` on the headers. +> If you have a [Demeter](https://demeter.run) port you would have to set the URL as `https://{host}` and on put `dmtr-api-key:YOUR_API_KEY` on the headers. + +# Examples + +In the `examples` folder you can find scripts demonstrating advanced capabilities. + +## Batch transactions + +This example shows how to send transactions to multiple recipients in a batch. + +**Location:** `examples/batch-transactions/` + +**Usage:** +```sh +./transfer.sh +``` +**Arguments:** +- `sender_wallet`: Name of the wallet sending the funds (e.g., `alice`) +- `receiver_wallets_list`: Comma-separated list of recipient wallet names (e.g., `bob,charlie,mark`) +- `lovelace_amount`: Amount in lovelaces to send to each recipient (e.g., `1000000`) +**Example:** +```sh +./transfer.sh alice bob,charlie,mark 1000000 +``` + +This will send 1,000,000 lovelaces from Alice's wallet to Bob, Charlie, and Mark individually. + +## Scheduled tasks + +This example demonstrates how to schedule recurring transactions using cron expressions. + +**Location:** `examples/scheduled-tasks/` + +**Usage:** +```sh +./transfer.sh +``` + +**Arguments:** +- `sender_wallet`: Name of the wallet sending the funds (e.g., `alice`) +- `receiver_wallet`: Name of the recipient wallet (e.g., `bob`) +- `lovelace_amount`: Amount in lovelaces to send (e.g., `1000000`) +- `cron_string`: Cron schedule expression in the format `'minute hour day month weekday'` + +**Example:** +```sh +./transfer.sh alice bob 1000000 '0 */2 * * *' +``` + +This will schedule a transfer of 1,000,000 lovelaces from Alice to Bob every 2 hours. + +## Complex transaction + +This example shows how to interact with a protocol that requires several parameters, specifically creating a ship in [Asteria](https://asteria.txpipe.io). + +**Location:** `examples/complex-transaction/` + +**Usage:** +```sh +./create-ship.sh +``` + +**Arguments:** +- `player_wallet`: Name of the player's wallet (e.g., `alice`) +- `pos_x`: X coordinate for the ship position (integer) +- `pos_y`: Y coordinate for the ship position (integer) + +**Example:** +```sh +./create-ship.sh alice 25 25 +``` +This will create a new ship for Alice at coordinates (25, 25) in [Asteria](https://asteria.txpipe.io). +> Note: you need to use a provider with the Cardano preview testnet in order to submit this transaction \ No newline at end of file diff --git a/dist-workspace.toml b/dist-workspace.toml index 6a0142f..f900c0a 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -8,11 +8,11 @@ cargo-dist-version = "0.29.0" # CI backends to support ci = "github" # The installers to generate for each app -installers = ["shell", "npm", "homebrew"] +installers = ["shell", "powershell", "npm", "homebrew"] # A GitHub repo to push Homebrew formulas to tap = "txpipe/homebrew-tap" # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu"] +targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] # Publish jobs to run in CI publish-jobs = ["homebrew"] # Which actions to run on pull requests diff --git a/docs/commands/_meta.yml b/docs/commands/_meta.yml index cf38a40..0daab62 100644 --- a/docs/commands/_meta.yml +++ b/docs/commands/_meta.yml @@ -1,3 +1,3 @@ label: Commands -order: 5 +order: 6 collapsed: true \ No newline at end of file diff --git a/docs/examples.mdx b/docs/examples.mdx new file mode 100644 index 0000000..e84b4c1 --- /dev/null +++ b/docs/examples.mdx @@ -0,0 +1,79 @@ +--- +title: Examples +sidebar: + order: 5 +--- + +In the CShell [repo](https://github.com/txpipe/cshell) you can find scripts demonstrating advanced capabilities. + +## Batch transactions + +This example shows how to send transactions to multiple recipients in a batch. + +**Location:** [`examples/batch-transactions/`](https://github.com/txpipe/cshell/tree/main/examples/batch-transactions) + +**Usage:** +```sh +./transfer.sh +``` + +**Arguments:** +- `sender_wallet`: Name of the wallet sending the funds (e.g., `alice`) +- `receiver_wallets_list`: Comma-separated list of recipient wallet names (e.g., `bob,charlie,mark`) +- `lovelace_amount`: Amount in lovelaces to send to each recipient (e.g., `1000000`) + +**Example:** +```sh +./transfer.sh alice bob,charlie,mark 1000000 +``` + +This will send 1,000,000 lovelaces from Alice's wallet to Bob, Charlie, and Mark individually. + +## Scheduled tasks + +This example demonstrates how to schedule recurring transactions using cron expressions. + +**Location:** [`examples/scheduled-tasks/`](https://github.com/txpipe/cshell/tree/main/examples/scheduled-tasks) + +**Usage:** +```sh +./transfer.sh +``` + +**Arguments:** +- `sender_wallet`: Name of the wallet sending the funds (e.g., `alice`) +- `receiver_wallet`: Name of the recipient wallet (e.g., `bob`) +- `lovelace_amount`: Amount in lovelaces to send (e.g., `1000000`) +- `cron_string`: Cron schedule expression in the format `'minute hour day month weekday'` + +**Example:** +```sh +./transfer.sh alice bob 1000000 '0 */2 * * *' +``` + +This will schedule a transfer of 1,000,000 lovelaces from Alice to Bob every 2 hours. + +## Complex transaction + +This example shows how to interact with a protocol that requires several parameters, specifically creating a ship in [Asteria](https://asteria.txpipe.io). + +**Location:** [`examples/complex-transaction/`](https://github.com/txpipe/cshell/tree/main/examples/complex-transaction) + +**Usage:** +```sh +./create-ship.sh +``` + +**Arguments:** +- `player_wallet`: Name of the player's wallet (e.g., `alice`) +- `pos_x`: X coordinate for the ship position (integer) +- `pos_y`: Y coordinate for the ship position (integer) + +**Example:** +```sh +./create-ship.sh alice 25 25 +``` + +This will create a new ship for Alice at coordinates (25, 25) in [Asteria](https://asteria.txpipe.io). + +> Note: you need to use a provider with the Cardano preview testnet in order to submit this transaction \ No newline at end of file diff --git a/docs/index.mdx b/docs/index.mdx index 35c7aff..5fc1b5b 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -26,6 +26,13 @@ In this guide we're going to learn how to go from zero to a running Cshell insta curl --proto '=https' --tlsv1.2 -LsSf https://github.com/txpipe/cshell/releases/latest/download/cshell-installer.sh | sh ``` + + You can use PowerShell to install Cshell on your system: + + ```sh + powershell -c "irm https://github.com/txpipe/cshell/releases/latest/download/cshell-installer.ps1 | iex" + ``` + You can use Npm to install Cshell on your system: diff --git a/examples/batch-transactions/transfer.sh b/examples/batch-transactions/transfer.sh new file mode 100644 index 0000000..b44119e --- /dev/null +++ b/examples/batch-transactions/transfer.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Validate arguments +if [ $# -ne 3 ]; then + echo "Error: exactly 3 arguments are required" + echo "Usage: $0 " + echo "Example: $0 alice bob,charlie,mark 1000000" + exit 1 +fi + +# Get arguments +SENDER_WALLET=$1 +RECEIVER_WALLETS=$2 +AMOUNT=$3 + +# Validate sender wallet name is not empty +if [ -z "$SENDER_WALLET" ]; then + echo "Error: sender wallet name cannot be empty" + exit 1 +fi + +# Validate receiver wallets list is not empty +if [ -z "$RECEIVER_WALLETS" ]; then + echo "Error: receiver wallets list cannot be empty" + exit 1 +fi + +# Validate amount is a positive integer +if ! [[ "$AMOUNT" =~ ^[0-9]+$ ]]; then + echo "Error: amount must be a positive integer" + exit 1 +fi + +# Get sender wallet info +SENDER_INFO=$(cshell wallet info --name $SENDER_WALLET 2>&1) +if [ $? -ne 0 ]; then + echo "Error: sender wallet '$SENDER_WALLET' does not exist" + exit 1 +fi + +SENDER_ADDRESS=$(echo "$SENDER_INFO" | grep "Address (testnet)" | awk '{print $5}') + +for RECEIVER_WALLET in ${RECEIVER_WALLETS//,/ }; do + + echo "Sending $AMOUNT lovelaces from $SENDER_WALLET to $RECEIVER_WALLET" + + RECEIVER_INFO=$(cshell wallet info --name $RECEIVER_WALLET 2>&1) + if [ $? -ne 0 ]; then + echo "Error: receiver wallet '$RECEIVER_WALLET' does not exist" + exit 1 + fi + + RECEIVER_ADDRESS=$(echo "$RECEIVER_INFO" | grep "Address (testnet)" | awk '{print $5}') + + cshell tx invoke \ + --tx3-file ./transfer.tx3 --signers $SENDER_WALLET --unsafe \ + --tx3-args-json "{\"sender\":\"$SENDER_ADDRESS\",\"receiver\":\"$RECEIVER_ADDRESS\",\"quantity\":$AMOUNT}" + + echo "" + +done \ No newline at end of file diff --git a/examples/batch-transactions/transfer.tx3 b/examples/batch-transactions/transfer.tx3 new file mode 100644 index 0000000..0acf87c --- /dev/null +++ b/examples/batch-transactions/transfer.tx3 @@ -0,0 +1,22 @@ +party Sender; + +party Receiver; + +tx transfer( + quantity: Int +) { + input source { + from: Sender, + min_amount: Ada(quantity) + fees, + } + + output { + to: Receiver, + amount: Ada(quantity), + } + + output { + to: Sender, + amount: source - Ada(quantity) - fees, + } +} diff --git a/examples/complex-transaction/asteria.tx3 b/examples/complex-transaction/asteria.tx3 new file mode 100644 index 0000000..ec392ef --- /dev/null +++ b/examples/complex-transaction/asteria.tx3 @@ -0,0 +1,135 @@ +party Player; + +policy SpacetimePolicy { + hash: 0xb6c5e14f31af0c92515ce156625afc4749e30ceef178cfae1f929fff, + ref: 0x81667fe89e20c352a8428f68efd0c9db1fef6cd15aa36ad4423797bb2c401431#1, +} + +policy AsteriaPolicy { + hash: 0xb16a0775a5e045b482ab2a98e241c9347f8ffe265bb6acd10452a6cc, + ref: 0x81667fe89e20c352a8428f68efd0c9db1fef6cd15aa36ad4423797bb2c401431#0, +} + +asset Fuel = 0x98b1c97b219c102dd0e9ba014481272d6ec069ec3ff47c63e291f1b7."FUEL"; +asset AdminToken = 0x516238dd0a79bac4bebe041c44bad8bf880d74720733d2fc0d255d28."asteriaAdmin"; + +type ShipDatum { + pos_x: Int, + pos_y: Int, + ship_token_name: Bytes, + pilot_token_name: Bytes, + last_move_latest_time: Int, +} + +type AsteriaDatum { + ship_counter: Int, + shipyard_policy: Bytes, +} + +type AsteriaRedeemer { + AddNewShip, + Mine, + ConsumeAsteria, +} + +type ShipyardRedeemer { + MintShip, + BurnShip, +} + +type FuelRedeemer { + MintFuel, + BurnFuel, +} + +tx create_ship( + p_pos_x: Int, // Ship Position X + p_pos_y: Int, // Ship Position Y + ship_name: Bytes, // Name of the ship + pilot_name: Bytes, // Name of the pilot + tip_slot: Int, // TODO: remove when tip_slot() implemented + last_move_timestamp: Int, +) { + locals { + initial_fuel: 5, // Should be taken from spaceTime datum + ship_mint_lovelace_fee: 1000000, // Should be taken from asteria script datum + spacetime_policy_hash: 0xb6c5e14f31af0c92515ce156625afc4749e30ceef178cfae1f929fff, + spacetime_policy_ref: 0x81667fe89e20c352a8428f68efd0c9db1fef6cd15aa36ad4423797bb2c401431#1, + asteria_policy_ref: 0x81667fe89e20c352a8428f68efd0c9db1fef6cd15aa36ad4423797bb2c401431#0, + pellet_policy_ref: 0x81667fe89e20c352a8428f68efd0c9db1fef6cd15aa36ad4423797bb2c401431#2, + } + + validity { + until_slot: tip_slot, // tip_slot() + 300 + } + + reference SpacetimeRef { + ref: spacetime_policy_ref, + } + + reference AsteriaRef { + ref: asteria_policy_ref, + } + + reference PelletRef { + ref: pellet_policy_ref, + } + + input* gas { + from: Player, + min_amount: fees + Ada(ship_mint_lovelace_fee) + min_utxo(pilot_token) + min_utxo(new_ship) + min_utxo(gas_change), + } + + input asteria { + from: AsteriaPolicy, + min_amount: AdminToken(1), + datum_is: AsteriaDatum, + redeemer: AsteriaRedeemer::AddNewShip {}, + } + + mint { + amount: AnyAsset(spacetime_policy_hash, pilot_name, 1) + AnyAsset(spacetime_policy_hash, ship_name, 1), + redeemer: ShipyardRedeemer::MintShip {}, + } + + mint { + amount: Fuel(initial_fuel), + redeemer: FuelRedeemer::MintFuel {}, + } + + output pilot_token { + to: Player, + amount: AnyAsset(spacetime_policy_hash, pilot_name, 1) + min_utxo(pilot_token), + } + + output updated_asteria { + to: AsteriaPolicy, + amount: asteria + Ada(ship_mint_lovelace_fee), + datum: AsteriaDatum { + ship_counter: asteria.ship_counter + 1, + shipyard_policy: asteria.shipyard_policy, + }, + } + + output new_ship { + to: SpacetimePolicy, + amount: AnyAsset(spacetime_policy_hash, ship_name, 1) + Fuel(initial_fuel) + min_utxo(new_ship), + datum: ShipDatum { + pos_x: p_pos_x, + pos_y: p_pos_y, + ship_token_name: ship_name, + pilot_token_name: pilot_name, + last_move_latest_time: last_move_timestamp, + }, + } + + output gas_change { + to: Player, + amount: gas - fees - Ada(ship_mint_lovelace_fee) - min_utxo(pilot_token) - min_utxo(new_ship), + } + + collateral { + from: Player, + min_amount: fees, + } +} \ No newline at end of file diff --git a/examples/complex-transaction/create-ship.sh b/examples/complex-transaction/create-ship.sh new file mode 100644 index 0000000..f624c00 --- /dev/null +++ b/examples/complex-transaction/create-ship.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +# Validate arguments +if [ $# -ne 3 ]; then + echo "Error: exactly 3 arguments are required" + echo "Usage: $0 " + echo "Example: $0 alice 25 25" + exit 1 +fi + +# Get arguments +PLAYER_WALLET=$1 +POS_X=$2 +POS_Y=$3 + +# Validate player wallet name is not empty +if [ -z "$PLAYER_WALLET" ]; then + echo "Error: player wallet name cannot be empty" + exit 1 +fi + +# Validate POS_X is an integer +if ! [[ "$POS_X" =~ ^-?[0-9]+$ ]]; then + echo "Error: pos_x must be an integer" + exit 1 +fi + +# Validate POS_Y is an integer +if ! [[ "$POS_Y" =~ ^-?[0-9]+$ ]]; then + echo "Error: pos_y must be an integer" + exit 1 +fi + +# Get player wallet info +PLAYER_INFO=$(cshell wallet info --name $PLAYER_WALLET 2>&1) +if [ $? -ne 0 ]; then + echo "Error: player wallet '$PLAYER_WALLET' does not exist" + exit 1 +fi + +PLAYER_ADDRESS=$(echo "$PLAYER_INFO" | grep "Address (testnet)" | awk '{print $5}') + +NEXT_SHIP_RES=$(curl --location 'https://8000-skillful-employee-kb9ou6.us1.demeter.run/graphql' --header 'Content-Type: application/json' \ + --data '{"query":"query { nextShipTokenName(spacetimeAddress: \"addr_test1wzmvtc20xxhseyj3tns4vcj6l3r5nccvamch3nawr7ffllcmwmxeq\", spacetimePolicyId: \"b6c5e14f31af0c92515ce156625afc4749e30ceef178cfae1f929fff\") { shipName pilotName } }"}') + +SHIP_NAME=$(echo $NEXT_SHIP_RES | jq -r '.data.nextShipTokenName.shipName') +PILOT_NAME=$(echo $NEXT_SHIP_RES | jq -r '.data.nextShipTokenName.pilotName') + +SHIP_NAME=$(printf '%s' "$SHIP_NAME" | xxd -p -u) +PILOT_NAME=$(printf '%s' "$PILOT_NAME" | xxd -p -u) + +LAST_SLOT_RES=$(curl --location 'https://8000-skillful-employee-kb9ou6.us1.demeter.run/graphql' --header 'Content-Type: application/json' \ + --data '{"query":"query { lastSlot { slot } }"}') + +TIP_SLOT=`expr $(echo $LAST_SLOT_RES | jq -r '.data.lastSlot.slot') + 300` + +LAST_MOVE_TIMESTAMP=`expr $(date +%s) + 300000` + +JSON=$(jq -n \ + --arg player "$PLAYER_ADDRESS" \ + --arg ship_name "$SHIP_NAME" \ + --arg pilot_name "$PILOT_NAME" \ + --argjson p_pos_x "$POS_X" \ + --argjson p_pos_y "$POS_Y" \ + --argjson tip_slot "$TIP_SLOT" \ + --argjson last_move_timestamp "$LAST_MOVE_TIMESTAMP" \ + '{ + "player": $player, + "ship_name": $ship_name, + "pilot_name": $pilot_name, + "p_pos_x": $p_pos_x, + "p_pos_y": $p_pos_y, + "tip_slot": $tip_slot, + "last_move_timestamp": $last_move_timestamp + }' +) + +cshell tx invoke --tx3-file ./asteria.tx3 --signers $PLAYER_WALLET --tx3-args-json "$JSON" --unsafe \ No newline at end of file diff --git a/examples/scheduled-tasks/transfer.sh b/examples/scheduled-tasks/transfer.sh new file mode 100644 index 0000000..03a12d5 --- /dev/null +++ b/examples/scheduled-tasks/transfer.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +# Validate arguments +if [ $# -ne 4 ]; then + echo "Error: exactly 4 arguments are required" + echo "Usage: $0 " + echo "Example: $0 alice bob 1000000 '0 */2 * * *'" + echo "Cron format: 'minute hour day month weekday'" + exit 1 +fi + +# Get arguments +SENDER_WALLET=$1 +RECEIVER_WALLET=$2 +AMOUNT=$3 +CRON_STRING=$4 + +# Validate sender wallet name is not empty +if [ -z "$SENDER_WALLET" ]; then + echo "Error: sender wallet name cannot be empty" + exit 1 +fi + +# Validate receiver wallet name is not empty +if [ -z "$RECEIVER_WALLET" ]; then + echo "Error: receiver wallet name cannot be empty" + exit 1 +fi + +# Validate amount is a positive integer +if ! [[ "$AMOUNT" =~ ^[0-9]+$ ]]; then + echo "Error: amount must be a positive integer" + exit 1 +fi + +# Validate cron string is not empty +if [ -z "$CRON_STRING" ]; then + echo "Error: cron string cannot be empty" + exit 1 +fi + +# Basic validation of cron string format (5 fields separated by spaces) +CRON_FIELDS=$(echo "$CRON_STRING" | awk '{print NF}') +if [ "$CRON_FIELDS" -ne 5 ]; then + echo "Error: invalid cron string format. Must have 5 fields: 'minute hour day month weekday'" + echo "Example: '0 */2 * * *' (every 2 hours)" + exit 1 +fi + +# Get sender wallet info +SENDER_INFO=$(cshell wallet info --name $SENDER_WALLET 2>&1) +if [ $? -ne 0 ]; then + echo "Error: sender wallet '$SENDER_WALLET' does not exist" + exit 1 +fi + +SENDER_ADDRESS=$(echo "$SENDER_INFO" | grep "Address (testnet)" | awk '{print $5}') + +# Get receiver wallet info +RECEIVER_INFO=$(cshell wallet info --name $RECEIVER_WALLET 2>&1) +if [ $? -ne 0 ]; then + echo "Error: receiver wallet '$RECEIVER_WALLET' does not exist" + exit 1 +fi + +RECEIVER_ADDRESS=$(echo "$RECEIVER_INFO" | grep "Address (testnet)" | awk '{print $5}') + +echo "Scheduling transfer of $AMOUNT lovelaces from $SENDER_WALLET to $RECEIVER_WALLET, cron schedule: $CRON_STRING" + +CSHELL_PATH=$(which cshell) +TX3_PATH=$(pwd)/transfer.tx3 +UNBUFFER_PATH=$(which unbuffer) +CMD="$CSHELL_PATH tx invoke --tx3-file $TX3_PATH --signers $SENDER_WALLET --unsafe --tx3-args-json '{\"sender\":\"$SENDER_ADDRESS\",\"receiver\":\"$RECEIVER_ADDRESS\",\"quantity\":$AMOUNT}'" + +crontab -l | { cat; echo "$CRON_STRING $UNBUFFER_PATH $CMD"; } | crontab - \ No newline at end of file diff --git a/examples/scheduled-tasks/transfer.tx3 b/examples/scheduled-tasks/transfer.tx3 new file mode 100644 index 0000000..0acf87c --- /dev/null +++ b/examples/scheduled-tasks/transfer.tx3 @@ -0,0 +1,22 @@ +party Sender; + +party Receiver; + +tx transfer( + quantity: Int +) { + input source { + from: Sender, + min_amount: Ada(quantity) + fees, + } + + output { + to: Receiver, + amount: Ada(quantity), + } + + output { + to: Sender, + amount: source - Ada(quantity) - fees, + } +}