Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zp/change ganache #51

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,8 @@ return Functions.encodeString(escape("$hello*world?"));
**_NOTE:_** The `simulateScript` function is a debugging tool and hence is not a perfect representation of the actual Chainlink oracle execution environment. Therefore, it is important to make a Functions request on a supported testnet blockchain before mainnet usage.

### Local Functions Testnet
> **Note**
> Anvil is required to use `localFunctionsTestnet`. Please refer to the [foundry book](https://book.getfoundry.sh) for Anvil [installation instructions](https://book.getfoundry.sh/getting-started/installation).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeuslawyer So there is no way to make this work without users installing Anvil manually first? If so, then IMO we should just keep using Ganache until we have a better solution. This is very inconvenient for devs and Ganache still works fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inclined to agree @KuphJr !


For debugging smart contracts and the end-to-end request flow on your local machine, you can use the `localFunctionsTestnet` function. This creates a local testnet RPC node with a mock Chainlink Functions contracts. You can then deploy your own Functions consumer contract to this local network, create and manage subscriptions, and send requests. Request processing will simulate the behavior of an actual DON where the request is executed 4 times and the discrete median response is transmitted back to the consumer contract. (Note that Chainlink Functions uses the following calculation to select the discrete median response: `const medianResponse = responses[responses.length - 1) / 2]`).

Expand All @@ -596,12 +598,14 @@ The `localFunctionsTestnet` function takes the following values as arguments.
```
const localFunctionsTestnet = await startLocalFunctionsTestnet(
simulationConfigPath?: string // Absolute path to config file which exports simulation config parameters
options?: ServerOptions, // Ganache server options
port?: number, // Defaults to 8545
options?: CreateAnvilOptions, // Anvil server options (See: https://www.npmjs.com/package/@viem/anvil#api and https://book.getfoundry.sh/reference/anvil/)
port = 8545
)
```

Observe that `localFunctionsTestnet` takes in a `simulationConfigPath` string as an optional argument. The primary reason for this is because the local testnet does not have the ability to access or decrypt encrypted secrets provided within request transactions. Instead, you can export an object named `secrets` from a TypeScript or JavaScript file and provide the absolute path to that file as the `simulationConfigPath` argument. When the JavaScript code is executed during the request, secrets specified in that file will be made accessible within the JavaScript code regardless of the `secretsLocation` or `encryptedSecretsReference` values sent in the request transaction. This config file can also contain other simulation config parameters. An example of this config file is shown below.
`options` is optional, and ships with a default `port` of 8545 and the default `test test test...junk` BIP39 mnemonic.

Observe that `localFunctionsTestnet` takes in a `simulationConfigPath` string as an optional argument. This is the path to a file that exports an object that has a `secrets` property on it. See [here](https://github.com/smartcontractkit/functions-hardhat-starter-kit/tree/main?tab=readme-ov-file#local-simulations-with-the-localfunctionstestnet) for an example. The primary reason for config property is because the local testnet does not have the ability to access or decrypt encrypted secrets provided within request transactions. Instead, you can export an object named `secrets` from a TypeScript or JavaScript file and provide the absolute path to that file as the `simulationConfigPath` argument. When the JavaScript code is executed during the request, secrets specified in that file will be made accessible within the JavaScript code regardless of the `secretsLocation` or `encryptedSecretsReference` values sent in the request transaction. This config file can also contain other simulation config parameters. An example of this config file is shown below.

```
export const secrets: { test: 'hello world' } // `secrets` object which can be accessed by the JavaScript code during request execution (can only contain string values)
Expand All @@ -619,7 +623,7 @@ export const maxQueryResponseBytes = 2097152 // Maximum size of incoming HTTP re

```
{
server: Server // Ganache server
server: Server // Anvil server
adminWallet: { address: string, privateKey: string } // Funded admin wallet
getFunds: (address: string, { weiAmount, juelsAmount }: { weiAmount?: BigInt | string; juelsAmount?: BigInt | string }) => Promise<void> // Method which can be called to send funds to any address
close: () => Promise<void> // Method to close the server
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"eth-crypto": "^2.6.0",
"ethers": "^5.7.2",
"ganache": "^7.9.1",
"uniq": "^1.0.1"
"uniq": "^1.0.1",
"@viem/anvil": "^0.0.7"
}
}
}
49 changes: 28 additions & 21 deletions src/localFunctionsTestnet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Wallet, Contract, ContractFactory, utils, providers } from 'ethers'
import Ganache from 'ganache'
import { ethers, Wallet, Contract, ContractFactory, utils } from 'ethers'
import { createAnvil } from '@viem/anvil'
import type { Anvil, CreateAnvilOptions } from '@viem/anvil'
import cbor from 'cbor'

import { simulateScript } from './simulateScript'
Expand All @@ -23,8 +24,6 @@ import {
TermsOfServiceAllowListSource,
} from './v1_contract_sources'

import type { ServerOptions } from 'ganache'

import type {
FunctionsRequestParams,
RequestCommitment,
Expand All @@ -36,27 +35,35 @@ import type {

export const startLocalFunctionsTestnet = async (
simulationConfigPath?: string,
options?: ServerOptions,
options?: CreateAnvilOptions,
port = 8545,
): Promise<LocalFunctionsTestnet> => {
const server = Ganache.server(options)
let localhost: string
let anvilNode: Anvil

server.listen(port, 'localhost', (err: Error | null) => {
if (err) {
throw Error(`Error starting local Functions testnet server:\n${err}`)
}
console.log(`Local Functions testnet server started on port ${port}`)
})
const junkMnemonic = 'test test test test test test test test test test test junk'

const accounts = server.provider.getInitialAccounts()
const firstAccount = Object.keys(accounts)[0]
const admin = new Wallet(
accounts[firstAccount].secretKey.slice(2),
new providers.JsonRpcProvider(`http://localhost:${port}`),
)
const anvilOpts = {
port,
mnemonic: junkMnemonic,
...options,
}

const contracts = await deployFunctionsOracle(admin)
try {
anvilNode = createAnvil(anvilOpts)
await anvilNode.start()
localhost = `http://${anvilNode.host}:${anvilNode.port}`
console.log(`Local Functions testnet server ${anvilNode.status} at ${localhost}...`)
} catch (error) {
throw Error(`Error starting local Functions testnet server:\n${error}`)
}

const provider = new ethers.providers.JsonRpcProvider(localhost)
let admin = Wallet.fromMnemonic(junkMnemonic)
console.info('Using admin wallet address: ', admin.address)
admin = admin.connect(provider)

const contracts = await deployFunctionsOracle(admin)
contracts.functionsMockCoordinatorContract.on(
'OracleRequest',
(
Expand Down Expand Up @@ -123,11 +130,11 @@ export const startLocalFunctionsTestnet = async (

const close = async (): Promise<void> => {
contracts.functionsMockCoordinatorContract.removeAllListeners('OracleRequest')
await server.close()
await anvilNode.stop()
}

return {
server,
server: anvilNode,
adminWallet: {
address: admin.address,
privateKey: admin.privateKey,
Expand Down
Loading
Loading