Skip to content

Commit 05dc9c8

Browse files
anihamdem30m
andauthored
Permit2 sdk (#1724)
* python sdk * js sdk * use viem getContractAddress * Update addresses and address comments * Fix lint issues --------- Co-authored-by: Amin Moghaddam <amin@pyth.network>
1 parent 87aea6f commit 05dc9c8

File tree

11 files changed

+376
-144
lines changed

11 files changed

+376
-144
lines changed

express_relay/sdk/js/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,5 @@ npm run simple-searcher -- \
7878
--chain-id op_sepolia \
7979
--private-key <YOUR-PRIVATE-KEY>
8080
```
81+
82+
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.

express_relay/sdk/js/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

express_relay/sdk/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/express-relay-evm-js",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"description": "Utilities for interacting with the express relay protocol",
55
"homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/express_relay/sdk/js",
66
"author": "Douro Labs",

express_relay/sdk/js/src/examples/simpleSearcher.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,12 @@ class SimpleSearcher {
5050
const bid = BigInt(argv.bid);
5151
// Bid info should be generated by evaluating the opportunity
5252
// here for simplicity we are using a constant bid and 24 hours of validity
53+
// TODO: generate nonce more intelligently, to reduce gas costs
54+
const nonce = BigInt(Math.floor(Math.random() * 2 ** 50));
5355
const bidParams = {
5456
amount: bid,
55-
validUntil: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)),
57+
nonce: nonce,
58+
deadline: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)),
5659
};
5760
const opportunityBid = await this.client.signOpportunityBid(
5861
opportunity,
@@ -99,7 +102,7 @@ const argv = yargs(hideBin(process.argv))
99102
.option("bid", {
100103
description: "Bid amount in wei",
101104
type: "string",
102-
default: "100",
105+
default: "20000000000000000",
103106
})
104107
.option("private-key", {
105108
description:

express_relay/sdk/js/src/index.ts

Lines changed: 95 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { components, paths } from "./serverTypes";
22
import createClient, {
33
ClientOptions as FetchClientOptions,
44
} from "openapi-fetch";
5-
import { Address, Hex, isAddress, isHex } from "viem";
5+
import { Address, Hex, isAddress, isHex, getContractAddress } from "viem";
66
import { privateKeyToAccount, signTypedData } from "viem/accounts";
77
import WebSocket from "isomorphic-ws";
88
import {
@@ -11,11 +11,12 @@ import {
1111
BidParams,
1212
BidStatusUpdate,
1313
Opportunity,
14-
EIP712Domain,
14+
OpportunityAdapterConfig,
1515
OpportunityBid,
1616
OpportunityParams,
1717
TokenAmount,
1818
BidsResponse,
19+
TokenPermissions,
1920
} from "./types";
2021

2122
export * from "./types";
@@ -59,6 +60,50 @@ export function checkTokenQty(token: {
5960
};
6061
}
6162

63+
export const OPPORTUNITY_ADAPTER_CONFIGS: Record<
64+
string,
65+
OpportunityAdapterConfig
66+
> = {
67+
op_sepolia: {
68+
chain_id: 11155420,
69+
opportunity_adapter_factory: "0xfA119693864b2F185742A409c66f04865c787754",
70+
opportunity_adapter_init_bytecode_hash:
71+
"0x3d71516d94b96a8fdca4e3a5825a6b41c9268a8e94610367e69a8462cc543533",
72+
permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
73+
weth: "0x74A4A85C611679B73F402B36c0F84A7D2CcdFDa3",
74+
},
75+
};
76+
77+
/**
78+
* Converts sellTokens, bidAmount, and callValue to permitted tokens
79+
* @param tokens List of sellTokens
80+
* @param bidAmount
81+
* @param callValue
82+
* @param weth
83+
* @returns List of permitted tokens
84+
*/
85+
function getPermittedTokens(
86+
tokens: TokenAmount[],
87+
bidAmount: bigint,
88+
callValue: bigint,
89+
weth: Address
90+
): TokenPermissions[] {
91+
const permitted: TokenPermissions[] = tokens.map(({ token, amount }) => ({
92+
token,
93+
amount,
94+
}));
95+
const wethIndex = permitted.findIndex(({ token }) => token === weth);
96+
const extraWethNeeded = bidAmount + callValue;
97+
if (wethIndex !== -1) {
98+
permitted[wethIndex].amount += extraWethNeeded;
99+
return permitted;
100+
}
101+
if (extraWethNeeded > 0) {
102+
permitted.push({ token: weth, amount: extraWethNeeded });
103+
}
104+
return permitted;
105+
}
106+
62107
export class Client {
63108
public clientOptions: ClientOptions;
64109
public wsOptions: WsOptions;
@@ -145,21 +190,11 @@ export class Client {
145190
});
146191
}
147192

148-
private convertEIP712Domain(
149-
eip712Domain: components["schemas"]["EIP712Domain"]
150-
): EIP712Domain {
151-
return {
152-
name: eip712Domain.name,
153-
version: eip712Domain.version,
154-
verifyingContract: checkAddress(eip712Domain.verifying_contract),
155-
chainId: BigInt(eip712Domain.chain_id),
156-
};
157-
}
158-
159193
/**
160194
* Converts an opportunity from the server to the client format
161195
* Returns undefined if the opportunity version is not supported
162196
* @param opportunity
197+
* @returns Opportunity in the converted client format
163198
*/
164199
private convertOpportunity(
165200
opportunity: components["schemas"]["OpportunityParamsWithMetadata"]
@@ -179,7 +214,6 @@ export class Client {
179214
targetCallValue: BigInt(opportunity.target_call_value),
180215
sellTokens: opportunity.sell_tokens.map(checkTokenQty),
181216
buyTokens: opportunity.buy_tokens.map(checkTokenQty),
182-
eip712Domain: this.convertEIP712Domain(opportunity.eip_712_domain),
183217
};
184218
}
185219

@@ -256,6 +290,7 @@ export class Client {
256290
/**
257291
* Fetches opportunities
258292
* @param chainId Chain id to fetch opportunities for. e.g: sepolia
293+
* @returns List of opportunities
259294
*/
260295
async getOpportunities(chainId?: string): Promise<Opportunity[]> {
261296
const client = createClient<paths>(this.clientOptions);
@@ -308,47 +343,77 @@ export class Client {
308343
* @param opportunity Opportunity to bid on
309344
* @param bidParams Bid amount and valid until timestamp
310345
* @param privateKey Private key to sign the bid with
346+
* @returns Signed opportunity bid
311347
*/
312348
async signOpportunityBid(
313349
opportunity: Opportunity,
314350
bidParams: BidParams,
315351
privateKey: Hex
316352
): Promise<OpportunityBid> {
317353
const types = {
318-
ExecutionParams: [
319-
{ name: "sellTokens", type: "TokenAmount[]" },
354+
PermitBatchWitnessTransferFrom: [
355+
{ name: "permitted", type: "TokenPermissions[]" },
356+
{ name: "spender", type: "address" },
357+
{ name: "nonce", type: "uint256" },
358+
{ name: "deadline", type: "uint256" },
359+
{ name: "witness", type: "OpportunityWitness" },
360+
],
361+
OpportunityWitness: [
320362
{ name: "buyTokens", type: "TokenAmount[]" },
321363
{ name: "executor", type: "address" },
322364
{ name: "targetContract", type: "address" },
323365
{ name: "targetCalldata", type: "bytes" },
324366
{ name: "targetCallValue", type: "uint256" },
325-
{ name: "validUntil", type: "uint256" },
326367
{ name: "bidAmount", type: "uint256" },
327368
],
328369
TokenAmount: [
329370
{ name: "token", type: "address" },
330371
{ name: "amount", type: "uint256" },
331372
],
373+
TokenPermissions: [
374+
{ name: "token", type: "address" },
375+
{ name: "amount", type: "uint256" },
376+
],
332377
};
333378

334379
const account = privateKeyToAccount(privateKey);
380+
const opportunityAdapterConfig =
381+
OPPORTUNITY_ADAPTER_CONFIGS[opportunity.chainId];
382+
const create2Address = getContractAddress({
383+
bytecodeHash:
384+
opportunityAdapterConfig.opportunity_adapter_init_bytecode_hash,
385+
from: opportunityAdapterConfig.opportunity_adapter_factory,
386+
opcode: "CREATE2",
387+
salt: `0x${account.address.replace("0x", "").padStart(64, "0")}`,
388+
});
389+
335390
const signature = await signTypedData({
336391
privateKey,
337392
domain: {
338-
...opportunity.eip712Domain,
339-
chainId: Number(opportunity.eip712Domain.chainId),
393+
name: "Permit2",
394+
verifyingContract: checkAddress(opportunityAdapterConfig.permit2),
395+
chainId: opportunityAdapterConfig.chain_id,
340396
},
341397
types,
342-
primaryType: "ExecutionParams",
398+
primaryType: "PermitBatchWitnessTransferFrom",
343399
message: {
344-
sellTokens: opportunity.sellTokens,
345-
buyTokens: opportunity.buyTokens,
346-
executor: account.address,
347-
targetContract: opportunity.targetContract,
348-
targetCalldata: opportunity.targetCalldata,
349-
targetCallValue: opportunity.targetCallValue,
350-
validUntil: bidParams.validUntil,
351-
bidAmount: bidParams.amount,
400+
permitted: getPermittedTokens(
401+
opportunity.sellTokens,
402+
bidParams.amount,
403+
opportunity.targetCallValue,
404+
checkAddress(opportunityAdapterConfig.weth)
405+
),
406+
spender: create2Address,
407+
nonce: bidParams.nonce,
408+
deadline: bidParams.deadline,
409+
witness: {
410+
buyTokens: opportunity.buyTokens,
411+
executor: account.address,
412+
targetContract: opportunity.targetContract,
413+
targetCalldata: opportunity.targetCalldata,
414+
targetCallValue: opportunity.targetCallValue,
415+
bidAmount: bidParams.amount,
416+
},
352417
},
353418
});
354419

@@ -369,7 +434,8 @@ export class Client {
369434
executor: bid.executor,
370435
permission_key: bid.permissionKey,
371436
signature: bid.signature,
372-
valid_until: bid.bid.validUntil.toString(),
437+
deadline: bid.bid.deadline.toString(),
438+
nonce: bid.bid.nonce.toString(),
373439
};
374440
}
375441

express_relay/sdk/js/src/serverTypes.d.ts

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ export interface paths {
2222
get: operations["bid_status"];
2323
};
2424
"/v1/opportunities": {
25-
/** Fetch all opportunities ready to be exectued. */
25+
/**
26+
* Fetch opportunities ready for execution or historical opportunities
27+
* @description depending on the mode. You need to provide `chain_id` for historical mode.
28+
* Opportunities are sorted by creation time in ascending order in historical mode.
29+
*/
2630
get: operations["get_opportunities"];
2731
/**
2832
* Submit an opportunity ready to be executed.
@@ -159,28 +163,6 @@ export interface components {
159163
ClientRequest: components["schemas"]["ClientMessage"] & {
160164
id: string;
161165
};
162-
EIP712Domain: {
163-
/**
164-
* @description The network chain id parameter for EIP712 domain.
165-
* @example 31337
166-
*/
167-
chain_id: string;
168-
/**
169-
* @description The name parameter for the EIP712 domain.
170-
* @example OpportunityAdapter
171-
*/
172-
name: string;
173-
/**
174-
* @description The verifying contract address parameter for the EIP712 domain.
175-
* @example 0xcA11bde05977b3631167028862bE2a173976CA11
176-
*/
177-
verifying_contract: string;
178-
/**
179-
* @description The version parameter for the EIP712 domain.
180-
* @example 1
181-
*/
182-
version: string;
183-
};
184166
ErrorBodyResponse: {
185167
error: string;
186168
};
@@ -190,24 +172,31 @@ export interface components {
190172
* @example 1000000000000000000
191173
*/
192174
amount: string;
175+
/**
176+
* @description The latest unix timestamp in seconds until which the bid is valid
177+
* @example 1000000000000000000
178+
*/
179+
deadline: string;
193180
/**
194181
* @description Executor address
195182
* @example 0x5FbDB2315678afecb367f032d93F642f64180aa2
196183
*/
197184
executor: string;
185+
/**
186+
* @description The nonce of the bid permit signature
187+
* @example 123
188+
*/
189+
nonce: string;
198190
/**
199191
* @description The opportunity permission key
200192
* @example 0xdeadbeefcafe
201193
*/
202194
permission_key: string;
203195
/** @example 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12 */
204196
signature: string;
205-
/**
206-
* @description The latest unix timestamp in seconds until which the bid is valid
207-
* @example 1000000000000000000
208-
*/
209-
valid_until: string;
210197
};
198+
/** @enum {string} */
199+
OpportunityMode: "live" | "historical";
211200
OpportunityParams: components["schemas"]["OpportunityParamsV1"] & {
212201
/** @enum {string} */
213202
version: "v1";
@@ -257,7 +246,6 @@ export interface components {
257246
* @example 1700000000000000
258247
*/
259248
creation_time: number;
260-
eip_712_domain: components["schemas"]["EIP712Domain"];
261249
/**
262250
* @description The opportunity unique id
263251
* @example obo3ee3e-58cc-4372-a567-0e02b2c3d479
@@ -306,6 +294,11 @@ export interface components {
306294
* @example op_sepolia
307295
*/
308296
chain_id: string;
297+
/**
298+
* @description The gas limit for the contract call.
299+
* @example 2000000
300+
*/
301+
gas_limit: string;
309302
/**
310303
* @description The unique id for bid.
311304
* @example obo3ee3e-58cc-4372-a567-0e02b2c3d479
@@ -388,7 +381,6 @@ export interface components {
388381
* @example 1700000000000000
389382
*/
390383
creation_time: number;
391-
eip_712_domain: components["schemas"]["EIP712Domain"];
392384
/**
393385
* @description The opportunity unique id
394386
* @example obo3ee3e-58cc-4372-a567-0e02b2c3d479
@@ -488,12 +480,28 @@ export interface operations {
488480
};
489481
};
490482
};
491-
/** Fetch all opportunities ready to be exectued. */
483+
/**
484+
* Fetch opportunities ready for execution or historical opportunities
485+
* @description depending on the mode. You need to provide `chain_id` for historical mode.
486+
* Opportunities are sorted by creation time in ascending order in historical mode.
487+
*/
492488
get_opportunities: {
493489
parameters: {
494490
query?: {
495491
/** @example op_sepolia */
496492
chain_id?: string | null;
493+
/** @description Get opportunities in live or historical mode */
494+
mode?: components["schemas"]["OpportunityMode"];
495+
/**
496+
* @description The permission key to filter the opportunities by. Used only in historical mode.
497+
* @example 0xdeadbeef
498+
*/
499+
permission_key?: string | null;
500+
/**
501+
* @description The time to get the opportunities from. Used only in historical mode.
502+
* @example 2024-05-23T21:26:57.329954Z
503+
*/
504+
from_time?: string | null;
497505
};
498506
};
499507
responses: {

0 commit comments

Comments
 (0)