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

Migrating ethwrite adapter to monorepo #290

Merged
merged 17 commits into from
Feb 22, 2021
Merged
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
3 changes: 2 additions & 1 deletion .github/strategy/adapters.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
"iex-cloud",
"cfbenchmarks",
"harmony",
"tiingo"
"tiingo",
"ethwrite"
]
},
"2-step": {
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules/
*.zip
dist
.DS_Store
cache
3 changes: 3 additions & 0 deletions ethwrite/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
...require('../.eslintrc.ts.js'),
}
56 changes: 56 additions & 0 deletions ethwrite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Chainlink External Adapter for writing to Ethereum-based Blockchains

This external adapter allows you to configure an endpoint and private key to sign and send transactions to external Ethereum-based blockchains.

A typical workflow of a Chainlink job for this external adapter could look like:

- Retrieve a piece of data from _some data source_
- Parse the desired field from that data source's response
- Utilize this adapter to write the value to ChainB
- Parse the transaction object from ChainB for the transaction hash
- Write the transaction hash from ChainB to ChainA

### Environment Variables

| Required? | Name | Description | Options | Defaults to |
| :-------: | :--------------: | :-----------------------------------------------------------------: | :-----: | :---------: |
| ✅ | RPC_URL | RPC endpoint for that client. For example `http://localhost:8545` | | |
| ✅ | PRIVATE_KEY | The private key of a funded account, to sign the transactions from. | | |
| ✅ | CONTRACT_ADDRESS | The contract address that the contract is deployed to. | | |

---

### Input Parameters

| Required? | Name | Description | Options | Defaults to |
| :-------: | :--------: | :-------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------: | :----------: |
| ✅ | exAddr | The address for sending the transaction to. | | |
| | funcId | The setter function to call: | [`0xc2b12a73`(for bytes32), `0xa53b1c1e`(for int256),`0xd2282dc5`(for uint256)] | `0xd2282dc5` |
| | dataType | Pass this only in case you need to encode the data(normally should be already encoded). | [`bytes32`, `int256`, `uint256`] | |
| | result | The result of the previous adapter | | |
| | dataToSend | If specified, this value will be sent instead of `result`. | | |

---

## Output Format

```json
{
"jobRunID": "1",
"data": {
"nonce": 1,
"gasPrice": { "type": "BigNumber", "hex": "0x04a817c800" },
"gasLimit": { "type": "BigNumber", "hex": "0x53d8" },
"to": "0xBb5696deFD9005e0CfD8bf40ae4C1f9beB6c109d",
"value": { "type": "BigNumber", "hex": "0x00" },
"data": "0xa53b1c1e0000000000000000000000000000000000000000000000000000000000000036",
"chainId": 1337,
"v": 2710,
"r": "0x58389e578005462f7e8d0c980cd4fca9e16ab4141882f56f98deec0b572da0b3",
"s": "0x17a977f57a88c3dbbab89beac60beb78ae8ab4dffd676831f84d14151c5b03d5",
"from": "0x78C696E4cA526f17380DAc7b6C5fd54B17F3f637",
"hash": "0x311b1e57342f61379bc597813c41bfa7c3535cfc2a49e61b2456c602dd07708e"
},
"statusCode": 200
}
```
43 changes: 43 additions & 0 deletions ethwrite/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@chainlink/ethwrite-adapter",
"version": "0.0.2",
"description": "Chainlink ethwrite adapter.",
"keywords": [
"ethtx",
"ethwrite"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"repository": {
"url": "https://github.com/smartcontractkit/external-adapters-js",
"type": "git"
},
"license": "MIT",
"scripts": {
"prepublishOnly": "yarn build && yarn test:unit",
"setup": "yarn build",
"build": "tsc -b",
"lint": "eslint --ignore-path ../.eslintignore . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint --ignore-path ../.eslintignore . --ext .js,.jsx,.ts,.tsx --fix",
"test": "mocha --delay --exit --timeout 0 -r ts-node/register 'test/**/*.test.ts'",
"test:unit": "mocha --delay --exit --grep @integration --invert -r ts-node/register 'test/**/*.test.ts'",
"test:integration": "mocha --delay --exit --timeout 0 --grep @integration -r ts-node/register 'test/**/*.test.ts'",
"server": "node -e 'require(\"./index.js\").server()'",
"server:dist": "node -e 'require(\"./dist/index.js\").server()'",
"start": "yarn server:dist"
},
"devDependencies": {
"@types/chai": "^4.2.11",
"@types/express": "^4.17.6",
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.13",
"@typescript-eslint/eslint-plugin": "^3.9.0",
"@typescript-eslint/parser": "^3.9.0",
"ts-node": "^8.10.2",
"typescript": "^3.9.7"
},
"dependencies": {}
}
35 changes: 35 additions & 0 deletions ethwrite/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Requester, Validator, AdapterError } from '@chainlink/external-adapter'
import { ExecuteWithConfig, ExecuteFactory } from '@chainlink/types'
import { makeConfig, DEFAULT_ENDPOINT, Config } from './config'
import { txsend } from './endpoint'

const inputParams = {
endpoint: false,
}

export const execute: ExecuteWithConfig<Config> = async (request, config) => {
const validator = new Validator(request, inputParams)
if (validator.error) throw validator.error

Requester.logConfig(config)

const jobRunID = validator.validated.id
const endpoint = validator.validated.data.endpoint || DEFAULT_ENDPOINT

switch (endpoint) {
case txsend.NAME: {
return await txsend.execute(request, config)
}
default: {
throw new AdapterError({
jobRunID,
message: `Endpoint ${endpoint} not supported.`,
statusCode: 400,
})
}
}
}

export const makeExecute: ExecuteFactory<Config> = (config) => {
return async (request) => execute(request, config || makeConfig())
}
19 changes: 19 additions & 0 deletions ethwrite/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { util } from '@chainlink/ea-bootstrap'

export type Config = {
rpcUrl: string
network?: string
privateKey: string
api: any
Copy link
Member

Choose a reason for hiding this comment

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

I think you can remove api right?

Copy link
Contributor Author

@ebarakos ebarakos Feb 18, 2021

Choose a reason for hiding this comment

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

If I want to use ExecuteWithConfig<C extends Config>, I can't, since it is required in main Config type. And I don't feel like changing the adapter.ts boilerplate, since IMHO should be removed at some point.

I was thinking of changing this type to ExecuteWithConfig<C> but I am considering of checking this again separately, maybe as part of #293.

I can also just use execute = async (input: AdapterRequest, config: Config): Promise<AdapterResponse> for the particular adapter as in synth-index though. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

You're right, the tweak is made is some places is defining the makeExecute as const makeExecute = (config?: Config): Execute => {...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's check changing it to either ExecuteWithConfig or removing the requirement of api param, in another PR, what do you think?

Copy link
Member

Choose a reason for hiding this comment

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

Let's leave it as it is. There's an issue open on Config inflexibility, we can leave it for another PR

Copy link
Collaborator

Choose a reason for hiding this comment

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

Tagging #217

}

export const DEFAULT_ENDPOINT = 'txsend'

export const makeConfig = (): Config => {
return {
api: {},
rpcUrl: util.getRequiredEnv('RPC_URL'),
network: util.getEnv('NETWORK') || 'mainnet',
privateKey: util.getRequiredEnv('PRIVATE_KEY'),
}
}
1 change: 1 addition & 0 deletions ethwrite/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as txsend from './txsend'
72 changes: 72 additions & 0 deletions ethwrite/src/endpoint/txsend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Requester, Validator, AdapterError } from '@chainlink/external-adapter'
import { ExecuteWithConfig } from '@chainlink/types'
import { ethers } from 'ethers'
import { Config } from '../config'

export const NAME = 'txsend'

const encode = (type: any, value: any) => {
let retVal
switch (type) {
case 'bytes32':
retVal = ethers.utils.formatBytes32String(value)
break
default:
retVal = ethers.utils.defaultAbiCoder.encode([type], [value])
break
}
return retVal.slice(2)
}

const customParams = {
exAddr: true,
funcId: false,
dataType: false,
result: false,
dataToSend: false,
}

export const execute: ExecuteWithConfig<Config> = async (request, config) => {
const provider = new ethers.providers.JsonRpcProvider(config.rpcUrl)
const wallet = new ethers.Wallet(config.privateKey, provider)

const validator = new Validator(request, customParams)
if (validator.error) throw validator.error

Copy link
Member

Choose a reason for hiding this comment

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

Load the provider and the wallet here, with the values from config

const getUint256 = '0xc2b12a73'

const jobRunID = validator.validated.id
const externalAddress = validator.validated.data.exAddr
const functionId = validator.validated.data.funcId || getUint256
// Passing this optionally, in case the data is not encrypted from the previous step
const dataType = validator.validated.data.dataType
// Prioritize data coming from a previous adapter (result),
// but allow dataToSend to be used if specified
const dataToSend = validator.validated.data.result || validator.validated.data.dataToSend || ''
// Ensure we use only 4 bytes for the functionId
let transactionData
if (dataType) {
transactionData = functionId.substring(0, 10) + encode(dataType, dataToSend)
} else {
transactionData = functionId.substring(0, 10) + dataToSend
}

const transaction = {
to: externalAddress,
data: transactionData,
}

try {
const tx = await wallet.sendTransaction(transaction)
return Requester.success(jobRunID, {
data: tx,
status: 200,
})
} catch (e) {
throw new AdapterError({
jobRunID,
message: e,
statusCode: 400,
})
}
}
7 changes: 7 additions & 0 deletions ethwrite/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { expose } from '@chainlink/ea-bootstrap'
import { makeExecute } from './adapter'
import { makeConfig } from './config'

const NAME = 'ETHWRITE'

export = { NAME, makeExecute, makeConfig, ...expose(makeExecute()) }
Loading