feat!(express_relay): Update bid's signature to eip712 (#1455)

This commit is contained in:
Dani Mehrjerdi 2024-04-18 18:55:18 +04:00 committed by GitHub
parent 8be6a9ad1c
commit 93efd61ea4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 179 additions and 92 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@pythnetwork/express-relay-evm-js",
"version": "0.1.1",
"version": "0.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "@pythnetwork/express-relay-evm-js",
"version": "0.2.1",
"version": "0.4.0",
"description": "Utilities for interacting with the express relay protocol",
"homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/express_relay/sdk/js",
"author": "Douro Labs",

View File

@ -2,15 +2,8 @@ import type { components, paths } from "./serverTypes";
import createClient, {
ClientOptions as FetchClientOptions,
} from "openapi-fetch";
import {
Address,
encodeAbiParameters,
Hex,
isAddress,
isHex,
keccak256,
} from "viem";
import { privateKeyToAccount, sign, signatureToHex } from "viem/accounts";
import { Address, Hex, isAddress, isHex } from "viem";
import { privateKeyToAccount, signTypedData } from "viem/accounts";
import WebSocket from "isomorphic-ws";
import {
Bid,
@ -18,6 +11,7 @@ import {
BidParams,
BidStatusUpdate,
Opportunity,
EIP712Domain,
OpportunityBid,
OpportunityParams,
TokenAmount,
@ -136,6 +130,17 @@ export class Client {
});
}
private convertEIP712Domain(
eip712Domain: components["schemas"]["EIP712Domain"]
): EIP712Domain {
return {
name: eip712Domain.name,
version: eip712Domain.version,
verifyingContract: checkAddress(eip712Domain.verifying_contract),
chainId: BigInt(eip712Domain.chain_id),
};
}
/**
* Converts an opportunity from the server to the client format
* Returns undefined if the opportunity version is not supported
@ -159,6 +164,7 @@ export class Client {
targetCallValue: BigInt(opportunity.target_call_value),
sellTokens: opportunity.sell_tokens.map(checkTokenQty),
buyTokens: opportunity.buy_tokens.map(checkTokenQty),
eip712Domain: this.convertEIP712Domain(opportunity.eip_712_domain),
};
}
@ -293,62 +299,54 @@ export class Client {
bidParams: BidParams,
privateKey: Hex
): Promise<OpportunityBid> {
const account = privateKeyToAccount(privateKey);
const convertTokenQty = ({ token, amount }: TokenAmount): [Hex, bigint] => [
token,
amount,
];
const payload = encodeAbiParameters(
[
{
name: "repayTokens",
type: "tuple[]",
components: [
{
type: "address",
},
{
type: "uint256",
},
],
},
{
name: "receiptTokens",
type: "tuple[]",
components: [
{
type: "address",
},
{
type: "uint256",
},
],
},
{ name: "contract", type: "address" },
{ name: "calldata", type: "bytes" },
{ name: "value", type: "uint256" },
{ name: "bid", type: "uint256" },
{ name: "validUntil", type: "uint256" },
const types = {
SignedParams: [
{ name: "executionParams", type: "ExecutionParams" },
{ name: "signer", type: "address" },
{ name: "deadline", type: "uint256" },
],
[
opportunity.sellTokens.map(convertTokenQty),
opportunity.buyTokens.map(convertTokenQty),
opportunity.targetContract,
opportunity.targetCalldata,
opportunity.targetCallValue,
bidParams.amount,
bidParams.validUntil,
]
);
ExecutionParams: [
{ name: "sellTokens", type: "TokenAmount[]" },
{ name: "buyTokens", type: "TokenAmount[]" },
{ name: "targetContract", type: "address" },
{ name: "targetCalldata", type: "bytes" },
{ name: "targetCallValue", type: "uint256" },
{ name: "bidAmount", type: "uint256" },
],
TokenAmount: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
],
};
const msgHash = keccak256(payload);
const account = privateKeyToAccount(privateKey);
const signature = await signTypedData({
privateKey,
domain: {
...opportunity.eip712Domain,
chainId: Number(opportunity.eip712Domain.chainId),
},
types,
primaryType: "SignedParams",
message: {
executionParams: {
sellTokens: opportunity.sellTokens,
buyTokens: opportunity.buyTokens,
targetContract: opportunity.targetContract,
targetCalldata: opportunity.targetCalldata,
targetCallValue: opportunity.targetCallValue,
bidAmount: bidParams.amount,
},
signer: account.address,
deadline: bidParams.validUntil,
},
});
const hash = signatureToHex(await sign({ hash: msgHash, privateKey }));
return {
permissionKey: opportunity.permissionKey,
bid: bidParams,
executor: account.address,
signature: hash,
signature,
opportunityId: opportunity.opportunityId,
};
}

View File

@ -144,6 +144,28 @@ export interface components {
ClientRequest: components["schemas"]["ClientMessage"] & {
id: string;
};
EIP712Domain: {
/**
* @description The network chain id parameter for EIP712 domain.
* @example 31337
*/
chain_id: string;
/**
* @description The name parameter for the EIP712 domain.
* @example OpportunityAdapter
*/
name: string;
/**
* @description The verifying contract address parameter for the EIP712 domain.
* @example 0xcA11bde05977b3631167028862bE2a173976CA11
*/
verifying_contract: string;
/**
* @description The version parameter for the EIP712 domain.
* @example 1
*/
version: string;
};
ErrorBodyResponse: {
error: string;
};
@ -220,6 +242,7 @@ export interface components {
* @example 1700000000000000
*/
creation_time: number;
eip_712_domain: components["schemas"]["EIP712Domain"];
/**
* @description The opportunity unique id
* @example obo3ee3e-58cc-4372-a567-0e02b2c3d479
@ -302,6 +325,7 @@ export interface components {
* @example 1700000000000000
*/
creation_time: number;
eip_712_domain: components["schemas"]["EIP712Domain"];
/**
* @description The opportunity unique id
* @example obo3ee3e-58cc-4372-a567-0e02b2c3d479

View File

@ -23,6 +23,27 @@ export type BidParams = {
*/
validUntil: bigint;
};
/**
* Represents the configuration for signing an opportunity
*/
export type EIP712Domain = {
/**
* The network chain id for the EIP712 domain.
*/
chainId: bigint;
/**
* The verifying contract address for the EIP712 domain.
*/
verifyingContract: Address;
/**
* The name parameter for the EIP712 domain.
*/
name: string;
/**
* The version parameter for the EIP712 domain.
*/
version: string;
};
/**
* Represents a valid opportunity ready to be executed
*/
@ -60,11 +81,18 @@ export type Opportunity = {
* Tokens to receive after the opportunity is executed
*/
buyTokens: TokenAmount[];
/**
* The data required to sign the opportunity
*/
eip712Domain: EIP712Domain;
};
/**
* All the parameters necessary to represent an opportunity
*/
export type OpportunityParams = Omit<Opportunity, "opportunityId">;
export type OpportunityParams = Omit<
Opportunity,
"opportunityId" | "eip712Domain"
>;
/**
* Represents a bid for an opportunity
*/

View File

@ -6,12 +6,9 @@ from typing import Callable, Any
from collections.abc import Coroutine
from uuid import UUID
import httpx
import web3
import websockets
from websockets.client import WebSocketClientProtocol
from eth_abi import encode
from eth_account.account import Account
from web3.auto import w3
from express_relay.types import (
Opportunity,
BidStatusUpdate,
@ -405,42 +402,73 @@ def sign_bid(
Returns:
A OpportunityBid object, representing the transaction to submit to the server. This object contains the searcher's signature.
"""
sell_tokens = [
(token.token, int(token.amount)) for token in opportunity.sell_tokens
]
buy_tokens = [(token.token, int(token.amount)) for token in opportunity.buy_tokens]
target_calldata = bytes.fromhex(opportunity.target_calldata.replace("0x", ""))
digest = encode(
[
"(address,uint256)[]",
"(address,uint256)[]",
"address",
"bytes",
"uint256",
"uint256",
"uint256",
executor = Account.from_key(private_key).address
domain_data = {
"name": opportunity.eip_712_domain.name,
"version": opportunity.eip_712_domain.version,
"chainId": opportunity.eip_712_domain.chain_id,
"verifyingContract": opportunity.eip_712_domain.verifying_contract,
}
message_types = {
"SignedParams": [
{"name": "executionParams", "type": "ExecutionParams"},
{"name": "signer", "type": "address"},
{"name": "deadline", "type": "uint256"},
],
[
sell_tokens,
buy_tokens,
opportunity.target_contract,
target_calldata,
opportunity.target_call_value,
bid_amount,
valid_until,
"ExecutionParams": [
{"name": "sellTokens", "type": "TokenAmount[]"},
{"name": "buyTokens", "type": "TokenAmount[]"},
{"name": "targetContract", "type": "address"},
{"name": "targetCalldata", "type": "bytes"},
{"name": "targetCallValue", "type": "uint256"},
{"name": "bidAmount", "type": "uint256"},
],
"TokenAmount": [
{"name": "token", "type": "address"},
{"name": "amount", "type": "uint256"},
],
}
# the data to be signed
message_data = {
"executionParams": {
"sellTokens": [
{
"token": token.token,
"amount": int(token.amount),
}
for token in opportunity.sell_tokens
],
"buyTokens": [
{
"token": token.token,
"amount": int(token.amount),
}
for token in opportunity.buy_tokens
],
"targetContract": opportunity.target_contract,
"targetCalldata": bytes.fromhex(
opportunity.target_calldata.replace("0x", "")
),
"targetCallValue": opportunity.target_call_value,
"bidAmount": bid_amount,
},
"signer": executor,
"deadline": valid_until,
}
signed_typed_data = Account.sign_typed_data(
private_key, domain_data, message_types, message_data
)
msg_data = web3.Web3.solidity_keccak(["bytes"], [digest])
signature = w3.eth.account.signHash(msg_data, private_key=private_key)
opportunity_bid = OpportunityBid(
opportunity_id=opportunity.opportunity_id,
permission_key=opportunity.permission_key,
amount=bid_amount,
valid_until=valid_until,
executor=Account.from_key(private_key).address,
signature=signature,
executor=executor,
signature=signed_typed_data,
)
return opportunity_bid

View File

@ -193,6 +193,13 @@ class OpportunityParams(BaseModel):
params: Union[OpportunityParamsV1] = Field(..., discriminator="version")
class EIP712Domain(BaseModel):
name: str
version: str
chain_id: IntString
verifying_contract: Address
class Opportunity(BaseModel):
"""
Attributes:
@ -206,6 +213,7 @@ class Opportunity(BaseModel):
version: The version of the opportunity.
creation_time: The creation time of the opportunity.
opportunity_id: The ID of the opportunity.
eip_712_domain: The EIP712 domain data needed for signing.
"""
target_calldata: HexString
@ -218,6 +226,7 @@ class Opportunity(BaseModel):
version: str
creation_time: IntString
opportunity_id: UUIDString
eip_712_domain: EIP712Domain
supported_versions: ClassVar[list[str]] = ["v1"]

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "express-relay"
version = "0.2.1"
version = "0.4.0"
description = "Utilities for searchers and protocols to interact with the Express Relay protocol."
authors = ["dourolabs"]
license = "Proprietary"

2
package-lock.json generated
View File

@ -1755,7 +1755,7 @@
},
"express_relay/sdk/js": {
"name": "@pythnetwork/express-relay-evm-js",
"version": "0.2.1",
"version": "0.4.0",
"license": "Apache-2.0",
"dependencies": {
"isomorphic-ws": "^5.0.0",