Skip to content

Commit 53521aa

Browse files
authored
feat: Update sdks to use svm opportunities (#2009)
* feat: Update sdks to use svm opportunities - Add necessary types for fetching SvmOpportunities - Remove relayerSigner and feeReceiverRelayer from configs since they are dynamic and should not be hardcoded in the sdk but fetched in the initialization of the client - Add fill rate option to the sample searchers - Add support for submitting SvmOpportunities in the js sdk (python not supported yet) - Put vm specific logic into separate files
1 parent c991c29 commit 53521aa

File tree

18 files changed

+1239
-571
lines changed

18 files changed

+1239
-571
lines changed

express_relay/examples/easy_lend/src/monitor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { hideBin } from "yargs/helpers";
33
import {
44
checkAddress,
55
Client,
6-
OpportunityParams,
6+
OpportunityCreate,
77
} from "@pythnetwork/express-relay-js";
88
import type { ContractFunctionReturnType } from "viem";
99
import {
@@ -133,7 +133,7 @@ class ProtocolMonitor {
133133
{ token: this.wethContract, amount: targetCallValue },
134134
];
135135
}
136-
const opportunity: OpportunityParams = {
136+
const opportunity: OpportunityCreate = {
137137
chainId: this.chainId,
138138
targetContract: this.vaultContract,
139139
targetCalldata: calldata,

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-js",
3-
"version": "0.10.0",
3+
"version": "0.11.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/const.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ export const OPPORTUNITY_ADAPTER_CONFIGS: Record<
2525

2626
export const SVM_CONSTANTS: Record<string, SvmConstantsConfig> = {
2727
"development-solana": {
28-
relayerSigner: new PublicKey(
29-
"GEeEguHhepHtPVo3E9RA1wvnxgxJ61iSc9dJfd433w3K"
30-
),
31-
feeReceiverRelayer: new PublicKey(
32-
"feesJcX9zwLiEZs9iQGXeBd65b9m2Zc1LjjyHngQF29"
33-
),
3428
expressRelayProgram: new PublicKey(
3529
"PytERJFhAKuNNuaiXkApLfWzwNwSNDACpigT3LwQfou"
3630
),

express_relay/sdk/js/src/evm.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import {
2+
Bid,
3+
BidParams,
4+
OpportunityBid,
5+
OpportunityEvm,
6+
TokenAmount,
7+
TokenPermissions,
8+
} from "./types";
9+
import { Address, encodeFunctionData, getContractAddress, Hex } from "viem";
10+
import { privateKeyToAccount, signTypedData } from "viem/accounts";
11+
import { checkAddress, ClientError } from "./index";
12+
import { OPPORTUNITY_ADAPTER_CONFIGS } from "./const";
13+
import { executeOpportunityAbi } from "./abi";
14+
15+
/**
16+
* Converts sellTokens, bidAmount, and callValue to permitted tokens
17+
* @param tokens List of sellTokens
18+
* @param bidAmount
19+
* @param callValue
20+
* @param weth
21+
* @returns List of permitted tokens
22+
*/
23+
function getPermittedTokens(
24+
tokens: TokenAmount[],
25+
bidAmount: bigint,
26+
callValue: bigint,
27+
weth: Address
28+
): TokenPermissions[] {
29+
const permitted: TokenPermissions[] = tokens.map(({ token, amount }) => ({
30+
token,
31+
amount,
32+
}));
33+
const wethIndex = permitted.findIndex(({ token }) => token === weth);
34+
const extraWethNeeded = bidAmount + callValue;
35+
if (wethIndex !== -1) {
36+
permitted[wethIndex].amount += extraWethNeeded;
37+
return permitted;
38+
}
39+
if (extraWethNeeded > 0) {
40+
permitted.push({ token: weth, amount: extraWethNeeded });
41+
}
42+
return permitted;
43+
}
44+
45+
function getOpportunityConfig(chainId: string) {
46+
const opportunityAdapterConfig = OPPORTUNITY_ADAPTER_CONFIGS[chainId];
47+
if (!opportunityAdapterConfig) {
48+
throw new ClientError(
49+
`Opportunity adapter config not found for chain id: ${chainId}`
50+
);
51+
}
52+
return opportunityAdapterConfig;
53+
}
54+
55+
export async function signBid(
56+
opportunity: OpportunityEvm,
57+
bidParams: BidParams,
58+
privateKey: Hex
59+
): Promise<Bid> {
60+
const opportunityAdapterConfig = getOpportunityConfig(opportunity.chainId);
61+
const executor = privateKeyToAccount(privateKey).address;
62+
const permitted = getPermittedTokens(
63+
opportunity.sellTokens,
64+
bidParams.amount,
65+
opportunity.targetCallValue,
66+
checkAddress(opportunityAdapterConfig.weth)
67+
);
68+
const signature = await getSignature(opportunity, bidParams, privateKey);
69+
70+
const calldata = makeAdapterCalldata(
71+
opportunity,
72+
permitted,
73+
executor,
74+
bidParams,
75+
signature
76+
);
77+
78+
return {
79+
amount: bidParams.amount,
80+
targetCalldata: calldata,
81+
chainId: opportunity.chainId,
82+
targetContract: opportunityAdapterConfig.opportunity_adapter_factory,
83+
permissionKey: opportunity.permissionKey,
84+
env: "evm",
85+
};
86+
}
87+
88+
/**
89+
* Constructs the calldata for the opportunity adapter contract.
90+
* @param opportunity Opportunity to bid on
91+
* @param permitted Permitted tokens
92+
* @param executor Address of the searcher's wallet
93+
* @param bidParams Bid amount, nonce, and deadline timestamp
94+
* @param signature Searcher's signature for opportunity params and bidParams
95+
* @returns Calldata for the opportunity adapter contract
96+
*/
97+
function makeAdapterCalldata(
98+
opportunity: OpportunityEvm,
99+
permitted: TokenPermissions[],
100+
executor: Address,
101+
bidParams: BidParams,
102+
signature: Hex
103+
): Hex {
104+
return encodeFunctionData({
105+
abi: [executeOpportunityAbi],
106+
args: [
107+
[
108+
[permitted, bidParams.nonce, bidParams.deadline],
109+
[
110+
opportunity.buyTokens,
111+
executor,
112+
opportunity.targetContract,
113+
opportunity.targetCalldata,
114+
opportunity.targetCallValue,
115+
bidParams.amount,
116+
],
117+
],
118+
signature,
119+
],
120+
});
121+
}
122+
123+
export async function getSignature(
124+
opportunity: OpportunityEvm,
125+
bidParams: BidParams,
126+
privateKey: Hex
127+
): Promise<`0x${string}`> {
128+
const types = {
129+
PermitBatchWitnessTransferFrom: [
130+
{ name: "permitted", type: "TokenPermissions[]" },
131+
{ name: "spender", type: "address" },
132+
{ name: "nonce", type: "uint256" },
133+
{ name: "deadline", type: "uint256" },
134+
{ name: "witness", type: "OpportunityWitness" },
135+
],
136+
OpportunityWitness: [
137+
{ name: "buyTokens", type: "TokenAmount[]" },
138+
{ name: "executor", type: "address" },
139+
{ name: "targetContract", type: "address" },
140+
{ name: "targetCalldata", type: "bytes" },
141+
{ name: "targetCallValue", type: "uint256" },
142+
{ name: "bidAmount", type: "uint256" },
143+
],
144+
TokenAmount: [
145+
{ name: "token", type: "address" },
146+
{ name: "amount", type: "uint256" },
147+
],
148+
TokenPermissions: [
149+
{ name: "token", type: "address" },
150+
{ name: "amount", type: "uint256" },
151+
],
152+
};
153+
154+
const account = privateKeyToAccount(privateKey);
155+
const executor = account.address;
156+
const opportunityAdapterConfig = getOpportunityConfig(opportunity.chainId);
157+
const permitted = getPermittedTokens(
158+
opportunity.sellTokens,
159+
bidParams.amount,
160+
opportunity.targetCallValue,
161+
checkAddress(opportunityAdapterConfig.weth)
162+
);
163+
const create2Address = getContractAddress({
164+
bytecodeHash:
165+
opportunityAdapterConfig.opportunity_adapter_init_bytecode_hash,
166+
from: opportunityAdapterConfig.opportunity_adapter_factory,
167+
opcode: "CREATE2",
168+
salt: `0x${executor.replace("0x", "").padStart(64, "0")}`,
169+
});
170+
171+
return signTypedData({
172+
privateKey,
173+
domain: {
174+
name: "Permit2",
175+
verifyingContract: checkAddress(opportunityAdapterConfig.permit2),
176+
chainId: opportunityAdapterConfig.chain_id,
177+
},
178+
types,
179+
primaryType: "PermitBatchWitnessTransferFrom",
180+
message: {
181+
permitted,
182+
spender: create2Address,
183+
nonce: bidParams.nonce,
184+
deadline: bidParams.deadline,
185+
witness: {
186+
buyTokens: opportunity.buyTokens,
187+
executor,
188+
targetContract: opportunity.targetContract,
189+
targetCalldata: opportunity.targetCalldata,
190+
targetCallValue: opportunity.targetCallValue,
191+
bidAmount: bidParams.amount,
192+
},
193+
},
194+
});
195+
}
196+
197+
export async function signOpportunityBid(
198+
opportunity: OpportunityEvm,
199+
bidParams: BidParams,
200+
privateKey: Hex
201+
): Promise<OpportunityBid> {
202+
const account = privateKeyToAccount(privateKey);
203+
const signature = await getSignature(opportunity, bidParams, privateKey);
204+
205+
return {
206+
permissionKey: opportunity.permissionKey,
207+
bid: bidParams,
208+
executor: account.address,
209+
signature,
210+
opportunityId: opportunity.opportunityId,
211+
};
212+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ class SimpleSearcherEvm {
4646
}
4747

4848
async opportunityHandler(opportunity: Opportunity) {
49+
if (!("targetContract" in opportunity))
50+
throw new Error("Not a valid EVM opportunity");
4951
const bidAmount = BigInt(argv.bid);
5052
// Bid info should be generated by evaluating the opportunity
5153
// here for simplicity we are using a constant bid and 24 hours of validity

0 commit comments

Comments
 (0)