Infura Ethereum (Independent Publisher) (#1410)

* Adds Infura Ethereum Connector v1

* Passing Swagger Verification

* Adds README.md

* Improved security and transformation docs

* Simplified usage of eth_getBalance method

* Update README.md to match template

* Remove empty properties from swagger.json

Co-authored-by: Sebastian Zolg <sebastian.zolg@swisscom.com>
This commit is contained in:
sebastianzolg 2022-03-08 04:10:42 +01:00 коммит произвёл GitHub
Родитель a5ce54a539
Коммит f891a6c83f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 482 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,69 @@
# Infura Ethereum Connector
Infura provides services to access the Ethereum Blockchain without your own Ethereum Node. This connector allows you to interact with the Ethereum Blockchain JSON-RPC API through Infura. Common use cases include checking your Ethereum account's (Wallet) balance, current gas fees, or information on the current block.
## Publisher: Sebastian Zolg
## Prerequisites
* An Infura [Free Plan](https://infura.io/pricing) (or higher)
* An Infura Ethereum Project
* The Project Id of the Infura Ethereum Project
## Supported Operations
The connector supports the following operations:
|Operation Id |Name |Description |Support |
|---------|---------|---------|---------|
|eth_getBlockNumber|Block Number|Returns the number of most recent block.|✅|
|eth_getBalance|Balance|Returns the balance of the account of given address.|✅|
|eth_gasPrice|Gas Price|Returns the current price per gas in wei.|✅|
## Obtaining Credentials
The Infura Ethereum Connector uses the Infura Project Id to communicate with the JSON-RPC endpoint. See the **Getting Started** section on how to get a new Project Id.
The **Project Id** is considered a secret and securely stored inside the Power Platform connection. Do not share the Project Id.
## Getting Started
1) Signup for an Infura [Free Plan](https://infura.io/pricing).
1) Go to your Infura [Dashboard](https://infura.io/dashboard) and click **Create New Project**.
1) Select **Ethereum** as Product, and provide a new name, e.g. *Power Platform Ethereum Connector*.
1) Inside your new project, click **Project Settings**.
1) Copy the **Project Id** (not the Project Secret).
1) Create a new Connection to the Infura Ethereum Connector, and provide the **Project Id**.
## Known Issues and Limitations
* Currently, there is no support for `basic` or `JWT token` authentication. Please refer to the [Securing Infura](#securing-infura) section for further details.
## Deployment Instructions
In addition to what is listed in the [Getting Started](#getting-started) section, please implement the connector according to the [Securing Infura](#securing-infura) section for additional security. Please refer to the [Transformation and Routing](#transformation-and-routing) section to better understand how the connector works.
### Securing Infura
As mentioned above, the Infura **Project Id** should be treated as a secret. Although the Power Platform is considered a secure environment, and the **Project Id** is stored as a secure string, it is used as part of the url path, which could result in leakage of your **Project Id**, e.g., through log files. There are more advanced security scenarios, such as JWT token auth, which isn't supported by this version of the connector.
To improve security, it is recommended to do the following:
1) Go to your Infura [Dashboard](https://infura.io/dashboard) and click **Settings** on your project.
1) Under **Security** configure `PER SECOND REQUESTS RATE-LIMITING` and `PER DAY TOTAL REQUESTS` according to your usage pattern.
1) If known in advance, add every account and contract address you interact with to the `CONTRACT ADDRESSES` list.
1) Set the allowed `ORIGINS` to `*.flow.microsoft.com`.
1) Limit `API REQUEST METHOD` to the methods you use, e.g., `eth_getBalance`. See [Supported Operations](#supported-operations).
### Transformation and Routing
#### Custom Code
This connector uses a custom code script (`script.csx`) to transform the API request from REST into JSON-RPC as used by the Infura API. JSON-RPC has a few downsides compared to REST when it comes to UX. To have the best possible UI support inside the Power Platform, we do the necessary transformation inside the connector's custom code.
The most important transformation steps are as follows:
* Flattening objects and their properties into a positional array understood by JSON-RPC.
* Transforming the response (`result`) from HEX encoded byte strings into decimal values for ease of use.
#### Path Templates
Unlike RESTful APIs, JSON-RPC doesn't rely on url path templates. Instead, every request hits the exact same url. The action to be invoked, such as `eth_getBalance` is provided as a `method` property in the post `body`. This pattern can't be modeled as OpenAPI (swagger) natively. Since the Power Platform experience relies on this pattern, we model the connector as a RESTful API and use a `Route request` policy template to redirect every incoming request to a single JSON-RPC endpoint. Furthermore, we use this approach to append the necessary **Project Id** to the path template on the fly. See the [apiProperties.json](apiProperties.json) file for details.

Просмотреть файл

@ -0,0 +1,248 @@
{
"swagger": "2.0",
"info": {
"title": "Infura Ethereum",
"description": "The Infura Ethereum Connector uses the Infura JSON-RPC API to access the Ethereum Blockchain.",
"version": "1.0",
"contact": {
"name": "Sebastian Zolg",
"url": "https://sebastianzolg.de",
"email": "sebastian.zolg@leantify.com"
}
},
"host": "mainnet.infura.io",
"basePath": "/v3",
"schemes": [
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/eth_gasPrice": {
"post": {
"responses": {
"200": {
"description": "default",
"schema": {
"type": "object",
"properties": {
"result": {
"type": "integer",
"description": "Result",
"title": "Result",
"x-ms-visibility": "important",
"format": "int32"
}
}
}
}
},
"summary": "Get Gas Price",
"description": "Returns the current price per gas in wei.",
"operationId": "Eth_gasPrice",
"x-ms-visibility": "important",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"jsonrpc": {
"type": "string",
"description": "jsonrpc",
"default": "2.0",
"x-ms-visibility": "internal"
},
"method": {
"type": "string",
"description": "method",
"default": "eth_gasPrice",
"x-ms-visibility": "internal"
},
"id": {
"type": "integer",
"format": "int32",
"description": "id",
"default": 1,
"x-ms-visibility": "internal"
}
},
"required": [
"id",
"jsonrpc",
"method"
]
}
}
]
}
},
"/eth_blockNumber": {
"post": {
"responses": {
"200": {
"description": "default",
"schema": {
"type": "object",
"properties": {
"result": {
"type": "integer",
"description": "Result",
"title": "Result",
"x-ms-visibility": "important",
"format": "int64"
}
}
}
}
},
"summary": "Get Block Number",
"description": "Returns the number of most recent block.",
"operationId": "Eth_blockNumber",
"x-ms-visibility": "important",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"jsonrpc": {
"type": "string",
"description": "jsonrpc",
"default": "2.0",
"x-ms-visibility": "internal"
},
"method": {
"type": "string",
"description": "method",
"default": "eth_blockNumber",
"x-ms-visibility": "internal"
},
"id": {
"type": "integer",
"format": "int32",
"description": "id",
"default": 1,
"x-ms-visibility": "internal"
}
},
"required": [
"id",
"jsonrpc",
"method"
]
}
}
]
}
},
"/eth_getBalance": {
"post": {
"responses": {
"200": {
"description": "default",
"schema": {
"type": "object",
"properties": {
"result": {
"type": "integer",
"description": "Result",
"title": "Result",
"x-ms-visibility": "important",
"format": "int64"
}
}
}
}
},
"summary": "Get Balance",
"description": "Returns the balance of the account of given address.",
"operationId": "Eth_getBalance",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"jsonrpc": {
"type": "string",
"description": "jsonrpc",
"default": "2.0",
"x-ms-visibility": "internal"
},
"method": {
"type": "string",
"description": "method",
"default": "eth_getBalance",
"x-ms-visibility": "internal"
},
"id": {
"type": "integer",
"format": "int32",
"description": "id",
"default": 1,
"x-ms-visibility": "internal"
},
"params": {
"type": "object",
"properties": {
"Address": {
"type": "string",
"description": "Address"
},
"Block": {
"type": "string",
"description": "Block",
"enum": [
"latest",
"earliest",
"pending"
],
"default": "latest"
}
}
}
},
"required": [
"id",
"jsonrpc",
"method",
"params"
]
}
}
]
}
}
},
"definitions": {},
"parameters": {},
"responses": {},
"securityDefinitions": {},
"security": [],
"tags": [],
"x-ms-connector-metadata": [
{
"propertyName": "Website",
"propertyValue": "https://infura.io/"
},
{
"propertyName": "Privacy policy",
"propertyValue": "https://consensys.net/privacy-policy"
},
{
"propertyName": "Categories",
"propertyValue": "Finance;Data"
}
]
}

Просмотреть файл

@ -0,0 +1,38 @@
{
"properties": {
"connectionParameters": {
"infuraProjectId": {
"type": "securestring",
"uiDefinition": {
"constraints": {
"required": "true",
"tabIndex": 2,
"clearText": false
},
"description": "The Id of your Infura Ethereum Project.",
"displayName": "Project Id",
"tooltip": "The Project Id can be found on the Infura Dashboard. Select your Ethereum Project and checkout the KEYS section."
}
}
},
"iconBrandColor": "#da3b01",
"scriptOperations": [
"Eth_blockNumber",
"Eth_getBalance",
"Eth_gasPrice"
],
"capabilities": [],
"policyTemplateInstances": [
{
"templateId": "routerequesttoendpoint",
"title": "Route to JSON-RPC Endpoint",
"parameters": {
"x-ms-apimTemplateParameter.newPath": "/@connectionParameters('infuraProjectId')",
"x-ms-apimTemplateParameter.httpMethod": "@Request.OriginalHTTPMethod"
}
}
],
"publisher": "Sebastian Zolg",
"stackOwner": "ConsenSys Software Inc."
}
}

Просмотреть файл

@ -0,0 +1,127 @@
public class Script : ScriptBase
{
public override async Task<HttpResponseMessage> ExecuteAsync()
{
// Transform the request before sending
await TransformRequestAsync();
// Forward the request
var response = await ForwardRequestAsync();
// Transform the response after receiving
response = await TransformResponseAsync(response);
return response;
}
public async Task TransformRequestAsync()
{
// Check if the operation ID matches what is specified in the OpenAPI definition of the connector
if (IsOperationId("Eth_getBalance"))
{
var contentAsString = await this.Context.Request.Content.ReadAsStringAsync().ConfigureAwait(false);
ConvertToPositionalParamsArray(contentAsString, "Address", "Block");
}
}
private async Task<HttpResponseMessage> ForwardRequestAsync()
{
// Use the context to forward/send an HTTP request
HttpResponseMessage response = await this.Context.SendAsync(this.Context.Request, this.CancellationToken)
.ConfigureAwait(continueOnCapturedContext: false);
return response;
}
private async Task<HttpResponseMessage> TransformResponseAsync(HttpResponseMessage response)
{
// Do the transformation if the response was successful
// Otherwise return error responses as-is
if (response.IsSuccessStatusCode)
{
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false);
var result = JObject.Parse(TryConvertRegexResult(responseString));
response.Content = CreateJsonContent(result.ToString());
}
return response;
}
private string TryConvertRegexResult(string contentAsString)
{
// NOTE: We assume the body of the incoming request looks like this:
// {
// "result": "<some hex encoded int>"
// }
// Parse as JSON object
var contentAsJson = JObject.Parse(contentAsString);
// Get the result field value
var resultField = (string)contentAsJson["result"];
// Check if it is hex encoded
if (IsHexEncoded(resultField))
{
// Convert result from hex to int64 for better usability
contentAsJson["result"] = ConvertHexToInt64(resultField);
}
return contentAsJson.ToString();
}
/// <summary>
/// Converts the given json object and its specified properties to a flat array with its values only.
/// </summary>
/// <param name="contentAsString">Json object as string</param>
/// <param name="properties">Property names to flatten in exact order</param>
private void ConvertToPositionalParamsArray(string contentAsString, params string[] properties)
{
// NOTE: We assume the body of the incoming request looks like this:
// {
// ...,
// "params":
// {
// "Address": "0x__________________",
// "Block": "latest"
// }
// }
//
// RESULT: Given this function was called for '(Address, Block)'
// {
// ...,
// "params": [
// "0x__________________",
// "latest"
// ]
// }
// Parse as JSON object
var contentAsJson = JObject.Parse(contentAsString);
JObject parameters = (JObject)contentAsJson["params"];
JArray positionalParams = new JArray();
for (int i = 0; i < properties.Length; i++)
{
positionalParams.Add(parameters[properties[i]]);
}
contentAsJson["params"] = positionalParams;
this.Context.Request.Content = CreateJsonContent(contentAsJson.ToString());
}
private bool IsHexEncoded(string candidate)
{
string HEX_ENCODED_UNSIGNED_INTEGER_REGEX = "^0x([1-9a-f]+[0-9a-f]*|0)$";
var rx = new Regex(HEX_ENCODED_UNSIGNED_INTEGER_REGEX);
return candidate != null ? rx.IsMatch(candidate) : false;
}
private long ConvertHexToInt64(string hexEncodedValue) => Convert.ToInt64(hexEncodedValue, 16);
private bool IsOperationId(string expected) => string.Equals(expected, this.Context.OperationId, StringComparison.InvariantCultureIgnoreCase);
}