Skip to content

Commit

Permalink
Permit2 sdk (#1724)
Browse files Browse the repository at this point in the history
* python sdk

* js sdk

* use viem getContractAddress

* Update addresses and address comments

* Fix lint issues

---------

Co-authored-by: Amin Moghaddam <[email protected]>
  • Loading branch information
anihamde and m30m authored Jun 26, 2024
1 parent 87aea6f commit 05dc9c8
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 144 deletions.
2 changes: 2 additions & 0 deletions express_relay/sdk/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,5 @@ npm run simple-searcher -- \
--chain-id op_sepolia \
--private-key <YOUR-PRIVATE-KEY>
```

Note that if you are using a localhost server at `http://127.0.0.1`, you should specify `--endpoint http://127.0.0.1:{PORT}` rather than `http://localhost:{PORT}`, as Typescript maps `localhost` to `::1` in line with IPv6 rather than to `127.0.0.1` as with IPv4.
2 changes: 1 addition & 1 deletion express_relay/sdk/js/package-lock.json

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

2 changes: 1 addition & 1 deletion express_relay/sdk/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pythnetwork/express-relay-evm-js",
"version": "0.6.0",
"version": "0.7.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",
Expand Down
7 changes: 5 additions & 2 deletions express_relay/sdk/js/src/examples/simpleSearcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@ class SimpleSearcher {
const bid = BigInt(argv.bid);
// Bid info should be generated by evaluating the opportunity
// here for simplicity we are using a constant bid and 24 hours of validity
// TODO: generate nonce more intelligently, to reduce gas costs
const nonce = BigInt(Math.floor(Math.random() * 2 ** 50));
const bidParams = {
amount: bid,
validUntil: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)),
nonce: nonce,
deadline: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)),
};
const opportunityBid = await this.client.signOpportunityBid(
opportunity,
Expand Down Expand Up @@ -99,7 +102,7 @@ const argv = yargs(hideBin(process.argv))
.option("bid", {
description: "Bid amount in wei",
type: "string",
default: "100",
default: "20000000000000000",
})
.option("private-key", {
description:
Expand Down
124 changes: 95 additions & 29 deletions express_relay/sdk/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { components, paths } from "./serverTypes";
import createClient, {
ClientOptions as FetchClientOptions,
} from "openapi-fetch";
import { Address, Hex, isAddress, isHex } from "viem";
import { Address, Hex, isAddress, isHex, getContractAddress } from "viem";
import { privateKeyToAccount, signTypedData } from "viem/accounts";
import WebSocket from "isomorphic-ws";
import {
Expand All @@ -11,11 +11,12 @@ import {
BidParams,
BidStatusUpdate,
Opportunity,
EIP712Domain,
OpportunityAdapterConfig,
OpportunityBid,
OpportunityParams,
TokenAmount,
BidsResponse,
TokenPermissions,
} from "./types";

export * from "./types";
Expand Down Expand Up @@ -59,6 +60,50 @@ export function checkTokenQty(token: {
};
}

export const OPPORTUNITY_ADAPTER_CONFIGS: Record<
string,
OpportunityAdapterConfig
> = {
op_sepolia: {
chain_id: 11155420,
opportunity_adapter_factory: "0xfA119693864b2F185742A409c66f04865c787754",
opportunity_adapter_init_bytecode_hash:
"0x3d71516d94b96a8fdca4e3a5825a6b41c9268a8e94610367e69a8462cc543533",
permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
weth: "0x74A4A85C611679B73F402B36c0F84A7D2CcdFDa3",
},
};

/**
* Converts sellTokens, bidAmount, and callValue to permitted tokens
* @param tokens List of sellTokens
* @param bidAmount
* @param callValue
* @param weth
* @returns List of permitted tokens
*/
function getPermittedTokens(
tokens: TokenAmount[],
bidAmount: bigint,
callValue: bigint,
weth: Address
): TokenPermissions[] {
const permitted: TokenPermissions[] = tokens.map(({ token, amount }) => ({
token,
amount,
}));
const wethIndex = permitted.findIndex(({ token }) => token === weth);
const extraWethNeeded = bidAmount + callValue;
if (wethIndex !== -1) {
permitted[wethIndex].amount += extraWethNeeded;
return permitted;
}
if (extraWethNeeded > 0) {
permitted.push({ token: weth, amount: extraWethNeeded });
}
return permitted;
}

export class Client {
public clientOptions: ClientOptions;
public wsOptions: WsOptions;
Expand Down Expand Up @@ -145,21 +190,11 @@ 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
* @param opportunity
* @returns Opportunity in the converted client format
*/
private convertOpportunity(
opportunity: components["schemas"]["OpportunityParamsWithMetadata"]
Expand All @@ -179,7 +214,6 @@ 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),
};
}

Expand Down Expand Up @@ -256,6 +290,7 @@ export class Client {
/**
* Fetches opportunities
* @param chainId Chain id to fetch opportunities for. e.g: sepolia
* @returns List of opportunities
*/
async getOpportunities(chainId?: string): Promise<Opportunity[]> {
const client = createClient<paths>(this.clientOptions);
Expand Down Expand Up @@ -308,47 +343,77 @@ export class Client {
* @param opportunity Opportunity to bid on
* @param bidParams Bid amount and valid until timestamp
* @param privateKey Private key to sign the bid with
* @returns Signed opportunity bid
*/
async signOpportunityBid(
opportunity: Opportunity,
bidParams: BidParams,
privateKey: Hex
): Promise<OpportunityBid> {
const types = {
ExecutionParams: [
{ name: "sellTokens", type: "TokenAmount[]" },
PermitBatchWitnessTransferFrom: [
{ name: "permitted", type: "TokenPermissions[]" },
{ name: "spender", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "witness", type: "OpportunityWitness" },
],
OpportunityWitness: [
{ name: "buyTokens", type: "TokenAmount[]" },
{ name: "executor", type: "address" },
{ name: "targetContract", type: "address" },
{ name: "targetCalldata", type: "bytes" },
{ name: "targetCallValue", type: "uint256" },
{ name: "validUntil", type: "uint256" },
{ name: "bidAmount", type: "uint256" },
],
TokenAmount: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
],
TokenPermissions: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
],
};

const account = privateKeyToAccount(privateKey);
const opportunityAdapterConfig =
OPPORTUNITY_ADAPTER_CONFIGS[opportunity.chainId];
const create2Address = getContractAddress({
bytecodeHash:
opportunityAdapterConfig.opportunity_adapter_init_bytecode_hash,
from: opportunityAdapterConfig.opportunity_adapter_factory,
opcode: "CREATE2",
salt: `0x${account.address.replace("0x", "").padStart(64, "0")}`,
});

const signature = await signTypedData({
privateKey,
domain: {
...opportunity.eip712Domain,
chainId: Number(opportunity.eip712Domain.chainId),
name: "Permit2",
verifyingContract: checkAddress(opportunityAdapterConfig.permit2),
chainId: opportunityAdapterConfig.chain_id,
},
types,
primaryType: "ExecutionParams",
primaryType: "PermitBatchWitnessTransferFrom",
message: {
sellTokens: opportunity.sellTokens,
buyTokens: opportunity.buyTokens,
executor: account.address,
targetContract: opportunity.targetContract,
targetCalldata: opportunity.targetCalldata,
targetCallValue: opportunity.targetCallValue,
validUntil: bidParams.validUntil,
bidAmount: bidParams.amount,
permitted: getPermittedTokens(
opportunity.sellTokens,
bidParams.amount,
opportunity.targetCallValue,
checkAddress(opportunityAdapterConfig.weth)
),
spender: create2Address,
nonce: bidParams.nonce,
deadline: bidParams.deadline,
witness: {
buyTokens: opportunity.buyTokens,
executor: account.address,
targetContract: opportunity.targetContract,
targetCalldata: opportunity.targetCalldata,
targetCallValue: opportunity.targetCallValue,
bidAmount: bidParams.amount,
},
},
});

Expand All @@ -369,7 +434,8 @@ export class Client {
executor: bid.executor,
permission_key: bid.permissionKey,
signature: bid.signature,
valid_until: bid.bid.validUntil.toString(),
deadline: bid.bid.deadline.toString(),
nonce: bid.bid.nonce.toString(),
};
}

Expand Down
70 changes: 39 additions & 31 deletions express_relay/sdk/js/src/serverTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ export interface paths {
get: operations["bid_status"];
};
"/v1/opportunities": {
/** Fetch all opportunities ready to be exectued. */
/**
* Fetch opportunities ready for execution or historical opportunities
* @description depending on the mode. You need to provide `chain_id` for historical mode.
* Opportunities are sorted by creation time in ascending order in historical mode.
*/
get: operations["get_opportunities"];
/**
* Submit an opportunity ready to be executed.
Expand Down Expand Up @@ -159,28 +163,6 @@ 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;
};
Expand All @@ -190,24 +172,31 @@ export interface components {
* @example 1000000000000000000
*/
amount: string;
/**
* @description The latest unix timestamp in seconds until which the bid is valid
* @example 1000000000000000000
*/
deadline: string;
/**
* @description Executor address
* @example 0x5FbDB2315678afecb367f032d93F642f64180aa2
*/
executor: string;
/**
* @description The nonce of the bid permit signature
* @example 123
*/
nonce: string;
/**
* @description The opportunity permission key
* @example 0xdeadbeefcafe
*/
permission_key: string;
/** @example 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12 */
signature: string;
/**
* @description The latest unix timestamp in seconds until which the bid is valid
* @example 1000000000000000000
*/
valid_until: string;
};
/** @enum {string} */
OpportunityMode: "live" | "historical";
OpportunityParams: components["schemas"]["OpportunityParamsV1"] & {
/** @enum {string} */
version: "v1";
Expand Down Expand Up @@ -257,7 +246,6 @@ 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
Expand Down Expand Up @@ -306,6 +294,11 @@ export interface components {
* @example op_sepolia
*/
chain_id: string;
/**
* @description The gas limit for the contract call.
* @example 2000000
*/
gas_limit: string;
/**
* @description The unique id for bid.
* @example obo3ee3e-58cc-4372-a567-0e02b2c3d479
Expand Down Expand Up @@ -388,7 +381,6 @@ 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
Expand Down Expand Up @@ -488,12 +480,28 @@ export interface operations {
};
};
};
/** Fetch all opportunities ready to be exectued. */
/**
* Fetch opportunities ready for execution or historical opportunities
* @description depending on the mode. You need to provide `chain_id` for historical mode.
* Opportunities are sorted by creation time in ascending order in historical mode.
*/
get_opportunities: {
parameters: {
query?: {
/** @example op_sepolia */
chain_id?: string | null;
/** @description Get opportunities in live or historical mode */
mode?: components["schemas"]["OpportunityMode"];
/**
* @description The permission key to filter the opportunities by. Used only in historical mode.
* @example 0xdeadbeef
*/
permission_key?: string | null;
/**
* @description The time to get the opportunities from. Used only in historical mode.
* @example 2024-05-23T21:26:57.329954Z
*/
from_time?: string | null;
};
};
responses: {
Expand Down
Loading

0 comments on commit 05dc9c8

Please sign in to comment.