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

Permit2 sdk #1724

Merged
merged 16 commits into from
Jun 26, 2024
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.
4 changes: 2 additions & 2 deletions express_relay/sdk/js/package-lock.json

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

4 changes: 2 additions & 2 deletions 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 Expand Up @@ -37,7 +37,7 @@
"isomorphic-ws": "^5.0.0",
"openapi-client-axios": "^7.5.4",
"openapi-fetch": "^0.8.2",
"openapi-typescript": "^6.5.5",
"openapi-typescript": "6.5.5",
"viem": "^2.7.6",
"ws": "^8.16.0"
},
Expand Down
18 changes: 14 additions & 4 deletions express_relay/sdk/js/src/examples/simpleSearcher.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { checkHex, Client } from "../index";
import { checkHex, Client, OPPORTUNITY_ADAPTER_CONFIGS } from "../index";
import { privateKeyToAccount } from "viem/accounts";
import { isHex } from "viem";
import { BidStatusUpdate, Opportunity } from "../types";
import {
BidStatusUpdate,
Opportunity,
OpportunityAdapterConfig,
} from "../types";

const DAY_IN_SECONDS = 60 * 60 * 24;

Expand Down Expand Up @@ -50,13 +54,19 @@ 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.random() * (2 ** 64 - 1));
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 opportunityAdapterConfig: OpportunityAdapterConfig =
OPPORTUNITY_ADAPTER_CONFIGS[opportunity.chainId];
const opportunityBid = await this.client.signOpportunityBid(
opportunity,
bidParams,
opportunityAdapterConfig,
checkHex(argv.privateKey)
);
try {
Expand Down Expand Up @@ -99,7 +109,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
136 changes: 107 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,14 @@ 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,
keccak256,
getContractAddress,
} from "viem";
import { privateKeyToAccount, signTypedData } from "viem/accounts";
import WebSocket from "isomorphic-ws";
import {
Expand All @@ -11,11 +18,12 @@ import {
BidParams,
BidStatusUpdate,
Opportunity,
EIP712Domain,
OpportunityAdapterConfig,
OpportunityBid,
OpportunityParams,
TokenAmount,
BidsResponse,
TokenPermissions,
} from "./types";

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

// TODO: update, remove "development" and replace with real chains
export const OPPORTUNITY_ADAPTER_CONFIGS: Record<
string,
OpportunityAdapterConfig
> = {
development: {
chain_id: 31337,
opportunity_adapter_factory: "0x610178da211fef7d417bc0e6fed39f05609ad788",
opportunity_adapter_init_bytecode_hash:
"0xfd1080f6c2d71672806f31108cb2f7d7709878e613b8d6bf028482184dcd70a4",
permit2: "0x8a791620dd6260079bf849dc5567adc3f2fdc318",
weth: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707",
},
};

/**
* 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[] = [];

for (let i = 0; i < tokens.length; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

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

use map

permitted.push({ token: tokens[i].token, amount: tokens[i].amount });
}

for (let i = 0; i < permitted.length; i++) {
if (permitted[i].token === weth) {
permitted[i].amount = permitted[i].amount + bidAmount + callValue;
return permitted;
}
}

if (bidAmount + callValue > 0) {
permitted.push({ token: weth, amount: bidAmount + callValue });
}

return permitted;
}

export class Client {
public clientOptions: ClientOptions;
public wsOptions: WsOptions;
Expand Down Expand Up @@ -145,21 +202,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 +226,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 +302,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 @@ -307,48 +354,78 @@ export class Client {
* Creates a signed bid for an opportunity
* @param opportunity Opportunity to bid on
* @param bidParams Bid amount and valid until timestamp
* @param opportunityAdapterConfig Opportunity adapter config
* @param privateKey Private key to sign the bid with
* @returns Signed opportunity bid
*/
async signOpportunityBid(
opportunity: Opportunity,
bidParams: BidParams,
opportunityAdapterConfig: OpportunityAdapterConfig,
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 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,
Copy link
Collaborator

Choose a reason for hiding this comment

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

nonce should have a default random value. Otherwise, we should document somewhere on what this value should be

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 +446,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
Loading
Loading