Explorar el Código

[entropy] Typescript code for coin flip example + docs (#1128)

* adding stuff

* add coin flip example

* it works

* docs

* hm

* pr comments
Jayant Krishnamurthy hace 2 años
padre
commit
f36e868ef6

+ 224 - 0
package-lock.json

@@ -20,6 +20,7 @@
         "target_chains/ethereum/entropy_sdk/solidity",
         "target_chains/ethereum/sdk/js",
         "target_chains/ethereum/sdk/solidity",
+        "target_chains/ethereum/examples/coin_flip/app",
         "target_chains/ethereum/examples/oracle_swap/app",
         "target_chains/sui/sdk/js",
         "target_chains/sui/cli",
@@ -11760,6 +11761,10 @@
       "resolved": "target_chains/ethereum/entropy_sdk/solidity",
       "link": true
     },
+    "node_modules/@pythnetwork/eth-coin-flip-example": {
+      "resolved": "target_chains/ethereum/examples/coin_flip/app",
+      "link": true
+    },
     "node_modules/@pythnetwork/eth-oracle-swap-example-frontend": {
       "resolved": "target_chains/ethereum/examples/oracle_swap/app",
       "link": true
@@ -57995,6 +58000,7 @@
       }
     },
     "target_chains/ethereum/entropy_sdk/solidity": {
+      "name": "@pythnetwork/entropy-sdk-solidity",
       "version": "0.1.0",
       "license": "Apache-2.0",
       "devDependencies": {
@@ -58042,6 +58048,133 @@
         "node": ">=10.0.0"
       }
     },
+    "target_chains/ethereum/examples/coin_flip/app": {
+      "name": "@pythnetwork/eth-coin-flip-example",
+      "version": "0.1.0",
+      "dependencies": {
+        "@pythnetwork/pyth-evm-js": "*",
+        "@pythnetwork/pyth-sdk-solidity": "*",
+        "@types/jest": "^27.5.2",
+        "@types/node": "^16.11.64",
+        "buffer": "^6.0.3",
+        "ethers": "^5.7.2",
+        "prettier": "^2.7.1",
+        "typescript": "^4.8.4"
+      },
+      "devDependencies": {
+        "ts-node": "^10.9.1"
+      }
+    },
+    "target_chains/ethereum/examples/coin_flip/app/node_modules/@types/jest": {
+      "version": "27.5.2",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz",
+      "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==",
+      "dependencies": {
+        "jest-matcher-utils": "^27.0.0",
+        "pretty-format": "^27.0.0"
+      }
+    },
+    "target_chains/ethereum/examples/coin_flip/app/node_modules/@types/node": {
+      "version": "16.18.60",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.60.tgz",
+      "integrity": "sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA=="
+    },
+    "target_chains/ethereum/examples/coin_flip/app/node_modules/ansi-styles": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+      "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "target_chains/ethereum/examples/coin_flip/app/node_modules/buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
+      }
+    },
+    "target_chains/ethereum/examples/coin_flip/app/node_modules/diff-sequences": {
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
+      "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==",
+      "engines": {
+        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+      }
+    },
+    "target_chains/ethereum/examples/coin_flip/app/node_modules/jest-diff": {
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
+      "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==",
+      "dependencies": {
+        "chalk": "^4.0.0",
+        "diff-sequences": "^27.5.1",
+        "jest-get-type": "^27.5.1",
+        "pretty-format": "^27.5.1"
+      },
+      "engines": {
+        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+      }
+    },
+    "target_chains/ethereum/examples/coin_flip/app/node_modules/jest-get-type": {
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz",
+      "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==",
+      "engines": {
+        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+      }
+    },
+    "target_chains/ethereum/examples/coin_flip/app/node_modules/jest-matcher-utils": {
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz",
+      "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==",
+      "dependencies": {
+        "chalk": "^4.0.0",
+        "jest-diff": "^27.5.1",
+        "jest-get-type": "^27.5.1",
+        "pretty-format": "^27.5.1"
+      },
+      "engines": {
+        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+      }
+    },
+    "target_chains/ethereum/examples/coin_flip/app/node_modules/pretty-format": {
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+      "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1",
+        "ansi-styles": "^5.0.0",
+        "react-is": "^17.0.1"
+      },
+      "engines": {
+        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+      }
+    },
+    "target_chains/ethereum/examples/coin_flip/app/node_modules/react-is": {
+      "version": "17.0.2",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+      "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+    },
     "target_chains/ethereum/examples/oracle_swap/app": {
       "name": "@pythnetwork/eth-oracle-swap-example-frontend",
       "version": "0.1.0",
@@ -66441,6 +66574,97 @@
         }
       }
     },
+    "@pythnetwork/eth-coin-flip-example": {
+      "version": "file:target_chains/ethereum/examples/coin_flip/app",
+      "requires": {
+        "@pythnetwork/pyth-evm-js": "*",
+        "@pythnetwork/pyth-sdk-solidity": "*",
+        "@types/jest": "^27.5.2",
+        "@types/node": "^16.11.64",
+        "buffer": "^6.0.3",
+        "ethers": "^5.7.2",
+        "prettier": "^2.7.1",
+        "ts-node": "^10.9.1",
+        "typescript": "^4.8.4"
+      },
+      "dependencies": {
+        "@types/jest": {
+          "version": "27.5.2",
+          "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz",
+          "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==",
+          "requires": {
+            "jest-matcher-utils": "^27.0.0",
+            "pretty-format": "^27.0.0"
+          }
+        },
+        "@types/node": {
+          "version": "16.18.60",
+          "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.60.tgz",
+          "integrity": "sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA=="
+        },
+        "ansi-styles": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+          "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
+        },
+        "buffer": {
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+          "requires": {
+            "base64-js": "^1.3.1",
+            "ieee754": "^1.2.1"
+          }
+        },
+        "diff-sequences": {
+          "version": "27.5.1",
+          "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
+          "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ=="
+        },
+        "jest-diff": {
+          "version": "27.5.1",
+          "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
+          "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==",
+          "requires": {
+            "chalk": "^4.0.0",
+            "diff-sequences": "^27.5.1",
+            "jest-get-type": "^27.5.1",
+            "pretty-format": "^27.5.1"
+          }
+        },
+        "jest-get-type": {
+          "version": "27.5.1",
+          "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz",
+          "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw=="
+        },
+        "jest-matcher-utils": {
+          "version": "27.5.1",
+          "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz",
+          "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==",
+          "requires": {
+            "chalk": "^4.0.0",
+            "jest-diff": "^27.5.1",
+            "jest-get-type": "^27.5.1",
+            "pretty-format": "^27.5.1"
+          }
+        },
+        "pretty-format": {
+          "version": "27.5.1",
+          "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+          "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+          "requires": {
+            "ansi-regex": "^5.0.1",
+            "ansi-styles": "^5.0.0",
+            "react-is": "^17.0.1"
+          }
+        },
+        "react-is": {
+          "version": "17.0.2",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+          "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+        }
+      }
+    },
     "@pythnetwork/eth-oracle-swap-example-frontend": {
       "version": "file:target_chains/ethereum/examples/oracle_swap/app",
       "requires": {

+ 1 - 0
package.json

@@ -15,6 +15,7 @@
     "target_chains/ethereum/entropy_sdk/solidity",
     "target_chains/ethereum/sdk/js",
     "target_chains/ethereum/sdk/solidity",
+    "target_chains/ethereum/examples/coin_flip/app",
     "target_chains/ethereum/examples/oracle_swap/app",
     "target_chains/sui/sdk/js",
     "target_chains/sui/cli",

+ 81 - 0
target_chains/ethereum/entropy_sdk/solidity/README.md

@@ -0,0 +1,81 @@
+# Pyth Entropy Solidity SDK
+
+The Pyth Entropy Solidity SDK allows you to generate secure random numbers on the blockchain by
+interacting with the Pyth Entropy protocol.
+This SDK can be used for any application that requires random numbers, such as NFT mints, gaming, and more.
+
+**WARNING**: The Entropy protocol is currently in testnet. It is **NOT INTENDED** for use in production applications.
+Use this protocol at your own risk.
+
+## Install
+
+TODO
+
+## Setup
+
+To use the SDK, you need the address of an Entropy contract on your blockchain and a randomness provider.
+The following table lists the current deployments of entropy.
+
+| Chain | Entropy Address | Provider |
+| avalanche-fuji | 0xD42c7a708E74AD19401D907a14146F006c851Ee3 | 0x368397bDc956b4F23847bE244f350Bde4615F25E
+| optimism-goerli | 0x28F16Af4D87523910b843a801454AEde5F9B0459 | 0x368397bDc956b4F23847bE244f350Bde4615F25E
+| eos-evm-testnet | 0xD42c7a708E74AD19401D907a14146F006c851Ee3 | 0x368397bDc956b4F23847bE244f350Bde4615F25E
+
+Choose one of these networks and instantiate an `IEntropy` contract in your solidity contract:
+
+```solidity
+ IEntropy entropy = IEntropy(<address>);
+```
+
+## Usage
+
+To generate a random number, follow these steps.
+
+### 1. Commit to a random number
+
+Generate a 32-byte random number on the client side, then hash it with keccak256 to create a commitment.
+You can do this with typescript and web3.js as follows:
+
+```typescript
+const randomNumber = web3.utils.randomHex(32);
+const commitment = web3.utils.keccak256(randomNumber);
+```
+
+### 2. Request a number from Entropy
+
+Invoke the `request` method of the `IEntropy` contract:
+
+```solidity
+uint64 sequenceNumber = entropy.request(provider, commitment, true)
+```
+
+This method returns a sequence number. Store this sequence number for use in later steps.
+If you are invoking this off-chain, the method also emits a `PythRandomEvents.Requested` event that contains the sequence number in it.
+
+### 3. Fetch the provider's number
+
+Fetch the provider's random number from them.
+For the provider `0x368397bDc956b4F23847bE244f350Bde4615F25E` you can query the webservice at https://fortuna-staging.pyth.network :
+
+```typescript
+await axios.get(
+  `https://fortuna-staging.pyth.network/v1/chains/${chainName}/revelations/${sequenceNumber}`
+);
+```
+
+This method returns a JSON object containing the provider's random number.
+
+### 4. Reveal the number
+
+Invoke the `reveal` method on the `IEntropy` contract:
+
+```solidity
+bytes32 randomNumber = entropy.reveal(
+    provider,
+    sequenceNumber,
+    randomNumber,
+    providerRandomNumber
+)
+```
+
+This method will combine the user and provider's random numbers, along with the blockhash, to construct the final secure random number.

+ 57 - 0
target_chains/ethereum/examples/coin_flip/README.md

@@ -0,0 +1,57 @@
+# Coin Flip Example application
+
+The coin flip example demonstrates how to use Pyth Entropy to flip a fair coin.
+
+## Try it out
+
+To try the example, first run the following commands from the root of the `pyth-crosschain` repository:
+
+```shell
+npm install
+npx lerna run build
+```
+
+These commands will build dependencies for the typescript project.
+
+Next, choose a network to run the example on.
+The example has been deployed on the following networks:
+
+| Chain Name | Address | RPC |
+| optimism-goerli | 0x075A5160FF6462924B4124595F6f987187496476 | https://goerli.optimism.io |
+
+You will also need the private key of a wallet with some gas tokens for your chosen network.
+Then, from the `coin_flip/app` directory, run the following command:
+
+```
+npm run flip-coin -- \
+  --private-key <hexadecimal evm private key> \
+  --chain-name <chain name> \
+  --address <address> \
+  --rpc-url <rpc url>
+```
+
+You can populate the arguments to this command from the table above.
+The command should print output like this:
+
+```text
+Running coin flip prototcol.
+1. Generating user's random number...
+   number    : 0x79b029406af43b11937bca98c49633f9382ed7d3fc0d60e110258c5c8f0d1a05
+   commitment: 0xd4bca63083f9fb9e83e68348cb48f45babd820fc3559c60ba9a67b0ab3845cea
+2. Requesting coin flip...
+   fee       : 87 wei
+   tx        : 0x3a59bb8c1aaa8c6ff97147bb3197e9b89c0d87174b0b6c32374fc62de6d8db94
+   sequence  : 50
+3. Retrieving provider's random number...
+   fetch url : https://fortuna-staging.pyth.network/v1/chains/optimism-goerli/revelations/50
+   number    : 0x760e53a19a4677ef671fde63db59462dfb3e09e94418e9962e2fa764026b8400
+4. Revealing the result of the coin flip...
+   tx        : 0x0549b93b12684187f73ddcaf8351ca4049867882c1b138989e15363a4d103220
+   result    : tails
+```
+
+## Understanding the Example
+
+The example consists of a Solidity contract and a Typescript script.
+See the extensive code comments in the contract at `contract/src/CoinFlip.sol` to learn how the example works.
+The typescript script is available at `app/src/flip_coin.ts` and demonstrates how to interact with the contract.

+ 4 - 0
target_chains/ethereum/examples/coin_flip/app/.gitignore

@@ -0,0 +1,4 @@
+node_modules
+lib
+.dccache
+*mnemonic*

+ 23 - 0
target_chains/ethereum/examples/coin_flip/app/package.json

@@ -0,0 +1,23 @@
+{
+  "name": "@pythnetwork/eth-coin-flip-example",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@pythnetwork/pyth-evm-js": "*",
+    "@pythnetwork/pyth-sdk-solidity": "*",
+    "@types/jest": "^27.5.2",
+    "@types/node": "^16.11.64",
+    "buffer": "^6.0.3",
+    "ethers": "^5.7.2",
+    "prettier": "^2.7.1",
+    "typescript": "^4.8.4"
+  },
+  "devDependencies": {
+    "ts-node": "^10.9.1"
+  },
+  "scripts": {
+    "build": "tsc",
+    "flip-coin": "npm run build && node lib/flip_coin.js",
+    "format": "prettier --write src/"
+  }
+}

+ 107 - 0
target_chains/ethereum/examples/coin_flip/app/src/CoinFlipAbi.json

@@ -0,0 +1,107 @@
+[
+  {
+    "inputs": [
+      {
+        "internalType": "address",
+        "name": "_entropy",
+        "type": "address"
+      },
+      {
+        "internalType": "address",
+        "name": "_entropyProvider",
+        "type": "address"
+      }
+    ],
+    "stateMutability": "nonpayable",
+    "type": "constructor"
+  },
+  {
+    "inputs": [],
+    "name": "IncorrectSender",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "InsufficientFee",
+    "type": "error"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": false,
+        "internalType": "uint64",
+        "name": "sequenceNumber",
+        "type": "uint64"
+      }
+    ],
+    "name": "FlipRequest",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": false,
+        "internalType": "bool",
+        "name": "isHeads",
+        "type": "bool"
+      }
+    ],
+    "name": "FlipResult",
+    "type": "event"
+  },
+  {
+    "inputs": [],
+    "name": "getFlipFee",
+    "outputs": [
+      {
+        "internalType": "uint256",
+        "name": "fee",
+        "type": "uint256"
+      }
+    ],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes32",
+        "name": "userCommitment",
+        "type": "bytes32"
+      }
+    ],
+    "name": "requestFlip",
+    "outputs": [],
+    "stateMutability": "payable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint64",
+        "name": "sequenceNumber",
+        "type": "uint64"
+      },
+      {
+        "internalType": "bytes32",
+        "name": "userRandom",
+        "type": "bytes32"
+      },
+      {
+        "internalType": "bytes32",
+        "name": "providerRandom",
+        "type": "bytes32"
+      }
+    ],
+    "name": "revealFlip",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "stateMutability": "payable",
+    "type": "receive"
+  }
+]

+ 123 - 0
target_chains/ethereum/examples/coin_flip/app/src/flip_coin.ts

@@ -0,0 +1,123 @@
+import Web3 from "web3";
+import yargs from "yargs";
+import { hideBin } from "yargs/helpers";
+import HDWalletProvider from "@truffle/hdwallet-provider";
+import CoinFlipAbi from "./CoinFlipAbi.json";
+import axios from "axios";
+
+const argv = yargs(hideBin(process.argv))
+  .option("private-key", {
+    description: "Private key (as a hexadecimal string) of the sender",
+    type: "string",
+    required: true,
+  })
+  .option("fortuna-url", {
+    description: "URL of the fortuna server for your chosen provider",
+    type: "string",
+    default: "https://fortuna-staging.pyth.network",
+  })
+  .option("chain-name", {
+    description:
+      "The name of your blockchain (for accessing data from fortuna)",
+    type: "string",
+    required: true,
+  })
+  .option("address", {
+    description: "The address of the CoinFlip contract",
+    type: "string",
+    required: true,
+  })
+  .option("rpc-url", {
+    description:
+      "The URL of an ETH RPC service for reading/writing to the blockchain",
+    type: "string",
+    required: true,
+  })
+  .help()
+  .alias("help", "h")
+  .parserConfiguration({
+    "parse-numbers": false,
+  })
+  .parseSync();
+
+const fortunaUrl = argv.fortunaUrl;
+const chainName = argv.chainName;
+const coinFlipContractAddress = argv.address;
+const rpc = argv.rpcUrl;
+const privateKey = argv.privateKey;
+
+async function fetchWithRetry(url: string, maxRetries: number): Promise<any> {
+  let retryCount = 0;
+
+  async function doRequest() {
+    try {
+      const response = await axios.get(url);
+      return response.data;
+    } catch (error) {
+      if (retryCount < maxRetries) {
+        retryCount++;
+        setTimeout(doRequest, 1000);
+      } else {
+        console.error("Max retry attempts reached. Exiting.");
+        throw error;
+      }
+    }
+  }
+
+  return await doRequest(); // Start the initial request
+}
+
+async function main() {
+  const provider = new HDWalletProvider({
+    privateKeys: [privateKey],
+    providerOrUrl: rpc,
+  });
+
+  const web3 = new Web3(provider as any);
+
+  const coinFlipContract = new web3.eth.Contract(
+    CoinFlipAbi as any,
+    coinFlipContractAddress
+  );
+
+  console.log(`Running coin flip prototcol.`);
+
+  console.log("1. Generating user's random number...");
+  const randomNumber = web3.utils.randomHex(32);
+  const commitment = web3.utils.keccak256(randomNumber);
+  console.log(`   number    : ${randomNumber}`);
+  console.log(`   commitment: ${commitment}`);
+
+  console.log("2. Requesting coin flip...");
+  const flipFee = await coinFlipContract.methods.getFlipFee().call();
+  console.log(`   fee       : ${flipFee} wei`);
+
+  const receipt = await coinFlipContract.methods
+    .requestFlip(commitment)
+    .send({ value: flipFee, from: provider.getAddress(0) });
+
+  console.log(`   tx        : ${receipt.transactionHash}`);
+  const sequenceNumber = receipt.events.FlipRequest.returnValues.sequenceNumber;
+  console.log(`   sequence  : ${sequenceNumber}`);
+
+  console.log("3. Retrieving provider's random number...");
+  const url = `${fortunaUrl}/v1/chains/${chainName}/revelations/${sequenceNumber}`;
+  console.log(`   fetch url : ${url}`);
+  // Note that there is a potential race condition here: the server may not have observed the request ^
+  // before this HTTP response. Hence, we retry fetching the url a couple of times.
+  const response = await fetchWithRetry(url, 3);
+  const providerRandom = web3.utils.bytesToHex(response.value);
+  console.log(`   number    : ${providerRandom}`);
+
+  console.log("4. Revealing the result of the coin flip...");
+  const receipt2 = await coinFlipContract.methods
+    .revealFlip(sequenceNumber, randomNumber, providerRandom)
+    .send({ from: provider.getAddress(0) });
+  console.log(`   tx        : ${receipt2.transactionHash}`);
+  const isHeads = receipt2.events.FlipResult.returnValues.isHeads;
+  console.log(`   result    : ${isHeads ? "heads" : "tails"}`);
+
+  provider.engine.stop();
+}
+
+main();

+ 15 - 0
target_chains/ethereum/examples/coin_flip/app/tsconfig.json

@@ -0,0 +1,15 @@
+{
+  "extends": "../../../../../tsconfig.base.json",
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "commonjs",
+    "declaration": true,
+    "rootDir": "src/",
+    "outDir": "./lib",
+    "strict": true,
+    "esModuleInterop": true,
+    "resolveJsonModule": true
+  },
+  "include": ["src", "src/*.json"],
+  "exclude": ["node_modules"]
+}

+ 4 - 4
target_chains/ethereum/examples/coin_flip/contract/scripts/deploy.sh

@@ -1,14 +1,14 @@
 #!/bin/bash -e
 
 # URL of the ethereum RPC node to use. Choose this based on your target network
-RPC_URL=https://api.avax-test.network/ext/bc/C/rpc
+RPC_URL=https://goerli.optimism.io
 
 # The address of the Pyth contract on your network. See the list of contract addresses here https://docs.pyth.network/documentation/pythnet-price-feeds/evm
-ENTROPY_CONTRACT_ADDRESS="0xD42c7a708E74AD19401D907a14146F006c851Ee3"
+ENTROPY_CONTRACT_ADDRESS="0x28F16Af4D87523910b843a801454AEde5F9B0459"
 PROVIDER="0x368397bDc956b4F23847bE244f350Bde4615F25E"
 
-# Avalanche fuji address:
-# 0x544c5ab499C38dff495724451783F63a3eeA40F2
+# Deployed contracts
+# Optimism goerli 0x075A5160FF6462924B4124595F6f987187496476
 
 # Note the -l here uses a ledger wallet to deploy your contract. You may need to change this
 # option if you are using a different wallet.

+ 6 - 0
target_chains/ethereum/examples/coin_flip/contract/src/CoinFlip.sol

@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: Apache 2
 pragma solidity ^0.8.0;
 
+// Import the entropy SDK in order to interact with the entropy contracts
 import "entropy-sdk-solidity/IEntropy.sol";
 
 library CoinFlipErrors {
@@ -73,6 +74,11 @@ contract CoinFlip {
         emit FlipRequest(sequenceNumber);
     }
 
+    // Get the fee to flip a coin. See the comment above about fees.
+    function getFlipFee() public returns (uint256 fee) {
+        fee = entropy.getFee(entropyProvider);
+    }
+
     // Reveal the result of the coin flip. The caller must have an in-flight request for a coin flip, which is
     // identified by `sequenceNumber`. The caller must additionally provide the random number that they previously
     // committed to, as well as the entropy provider's random number. The provider's random number can be retrieved