diff --git a/.github/workflows/playground-check.yaml b/.github/workflows/playground-check.yaml new file mode 100644 index 0000000000..7463647b0b --- /dev/null +++ b/.github/workflows/playground-check.yaml @@ -0,0 +1,90 @@ +name: Playground operation check + +on: + pull_request: + schedule: + # Run this job once per day + - cron: "0 0 * * *" + +jobs: + playground-check: + runs-on: ubuntu-latest + env: + FORK_URL_MAINNET: ${{ secrets.FORK_URL_MAINNET }} + steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: false + docker-images: false + swap-storage: false + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependency + run: sudo apt-get -qq update && sudo apt-get -y -q install curl jq + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Setup environment + run: | + cp playground/.env.example playground/.env + sed -i -e 's/ETH_RPC_URL=.*/ETH_RPC_URL=$FORK_URL_MAINNET/g' playground/.env + cat playground/.env + + - name: Start docker containers + id: containers + run: | + cd playground + # firstly start explorer container to workaround yarn cache problem + docker compose -f docker-compose.fork.yml up -d explorer + docker compose -f docker-compose.fork.yml up -d + + - name: Execute validation script + id: test_script + run: | + cd playground + ./test_playground.sh + + - name: Collect docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v2 + with: + dest: './logs' + + - name: Tar logs + if: failure() + run: tar cvzf ./logs.tgz ./logs + + - name: Upload logs to GitHub + if: failure() + id: artifact-upload-step + uses: actions/upload-artifact@v4 + with: + name: logs.tgz + path: ./logs.tgz + + - name: Slack notification on failure + if: failure() + id: slack + uses: slackapi/slack-github-action@v1.27.0 + with: + payload: | + { + "channel_id": "C037PB929ME", + "job_link":"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "actor":"${{ github.actor }}", + "event_name":"${{ github.event_name }}", + "head_ref":"${{ github.head_ref }}", + "step_containers":"${{ steps.containers.conclusion }}", + "step_test_script":"${{ steps.test_script.conclusion }}", + "log_artifcat":"${{ steps.artifact-upload-step.outputs.artifact-url }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_PLAYGROUND_CI_WEBHOOK_URL }} \ No newline at end of file diff --git a/playground/test_playground.sh b/playground/test_playground.sh new file mode 100755 index 0000000000..b49b7583b2 --- /dev/null +++ b/playground/test_playground.sh @@ -0,0 +1,155 @@ +#!/bin/bash + +# Fail on all errors +set -e +# Fail on expand of unset variables +set -u + +# Setup parameters +HOST=localhost:8080 +WETH_ADDRESS="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" # WETH token +SELL_TOKEN=$WETH_ADDRESS +BUY_TOKEN="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" # USDC token +SELL_AMOUNT="1000000000000000000" # 1 ETH +SLIPPAGE=2 # 2% +COW_SETTLMENT_CONTRACT="0x9008D19f58AAbD9eD0D60971565AA8510560ab41" +COW_VAULT_RELAYER_CONTRACT="0xC92E8bdf79f0507f65a392b0ab4667716BFE0110" +APPDATA='{"version":"1.3.0","metadata":{}}' +MAXUINT256="115792089237316195423570985008687907853269984665640564039457584007913129639935" + +# Following private key is only used for testing purposes in a local environment. +# For security reasons please do not use it on a production network, most likely +# all funds sent to this account will be stolen immediately. +PRIVATE_KEY="0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" + +# Wait for 2 minutes for all services are read +echo "Waiting until all services are ready" +curl --retry 24 --retry-delay 5 --retry-all-errors --fail-with-body -s --show-error \ + -H 'accept:application/json' \ + http://$HOST/api/v1/token/$BUY_TOKEN/native_price > /dev/null + +# Run test flow +echo "Using private key:" $PRIVATE_KEY +receiver=$(cast wallet address $PRIVATE_KEY) + +# Calculate AppData hash +app_data_hash=$(cast keccak $APPDATA) + +# Deposit WETH +echo "Wrapping some ETH" +docker exec playground-chain-1 cast send --private-key $PRIVATE_KEY --value 3ether $WETH_ADDRESS > /dev/null + +echo "Setting WETH allowance" +docker exec playground-chain-1 cast send --private-key $PRIVATE_KEY $WETH_ADDRESS "approve(address, uint)" $COW_VAULT_RELAYER_CONTRACT $MAXUINT256 > /dev/null + +echo "Request price qoute for buying USDC for WETH" +quote_response=$( curl --retry 5 --fail-with-body -s --show-error -X 'POST' \ + "http://$HOST/api/v1/quote" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "sellToken": "'$SELL_TOKEN'", + "buyToken": "'$BUY_TOKEN'", + "from": "'$receiver'", + "receiver": "'$receiver'", + "sellTokenBalance": "erc20", + "buyTokenBalance": "erc20", + "signingScheme": "eip712", + "onchainOrder": false, + "partiallyFillable": false, + "kind": "sell", + "sellAmountBeforeFee": "'$SELL_AMOUNT'" +}') + +buyAmount=$(jq -r --args '.quote.buyAmount' <<< "${quote_response}") +feeAmount=$(jq -r --args '.quote.feeAmount' <<< "${quote_response}") +validTo=$(($(date +%s) + 120)) # validity time: now + 2 minutes +sellAmount=$((SELL_AMOUNT - feeAmount)) + +# Apply slippage +buyAmount=$((buyAmount * ( 100 - $SLIPPAGE ) / 100 )) # apply slippage + +# Prepare EIP712 message +eip712_message=$(jq -r --args ' + .quote|=(.appData="'$app_data_hash'") | + .quote|=(.sellAmount="'$sellAmount'") | + .quote|=(.feeAmount="0") | + .quote|=(.buyAmount="'$buyAmount'") | + .quote|=(.validTo='$validTo') | + .quote' <<< "${quote_response}") + +# Prepare EIP-712 typed struct +eip712_typed_struct='{ + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Order": [ + { "name": "sellToken", "type": "address" }, + { "name": "buyToken", "type": "address" }, + { "name": "receiver", "type": "address" }, + { "name": "sellAmount", "type": "uint256" }, + { "name": "buyAmount", "type": "uint256" }, + { "name": "validTo", "type": "uint32" }, + { "name": "appData", "type": "bytes32" }, + { "name": "feeAmount", "type": "uint256" }, + { "name": "kind", "type": "string" }, + { "name": "partiallyFillable", "type": "bool" }, + { "name": "sellTokenBalance", "type": "string" }, + { "name": "buyTokenBalance", "type": "string" } + ] + }, + "primaryType": "Order", + "domain": { + "name": "Gnosis Protocol", + "version": "v2", + "chainId": 1, + "verifyingContract": "'$COW_SETTLMENT_CONTRACT'" + }, + "message": '$eip712_message' +}' + +# Compact json and escape quotes +eip712_typed_struct=$(jq -r -c <<< "${eip712_typed_struct}") +eip712_typed_struct=${eip712_typed_struct//\"/\\\"} + +# Sign quote_response with private key +signature=$(cast wallet sign --private-key $PRIVATE_KEY --data \""$eip712_typed_struct"\") +echo "Intent signature:" $signature + +app_data=${APPDATA//\"/\\\"} # escape quotes for json field + +# Update EIP712 message with additional fields required for order submit +order_proposal=$(jq -r -c --args ' + .from="'$receiver'" | + .appData|="'$app_data'" | + .appDataHash="'$app_data_hash'" | + .signature="'$signature'"' <<< "${eip712_message}") + +echo "Submit an order" +orderUid=$( curl --retry 5 --fail-with-body -s --show-error -X 'POST' \ + "http://$HOST/api/v1/orders" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d "${order_proposal}") +orderUid=${orderUid:1:-1} # remove quotes +echo "Order UID: $orderUid" + +for i in $(seq 1 24); +do + orderStatus=$( curl --retry 5 --fail-with-body -s --show-error -X 'GET' \ + "http://$HOST/api/v1/orders/$orderUid/status" \ + -H 'accept: application/json' | jq -r '.type') + echo -e -n "Order status: $orderStatus \r" + if [ "$orderStatus" = "traded" ]; then + echo -e "\nSuccess" + exit 0 + fi + sleep 5 +done + +echo -e "\nTimeout" +exit 1