Skip to content

feat: Update sdks to use svm opportunities #2009

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

Merged
merged 8 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions express_relay/examples/easy_lend/src/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { hideBin } from "yargs/helpers";
import {
checkAddress,
Client,
OpportunityParams,
OpportunityCreate,
} from "@pythnetwork/express-relay-js";
import type { ContractFunctionReturnType } from "viem";
import {
Expand Down Expand Up @@ -133,7 +133,7 @@ class ProtocolMonitor {
{ token: this.wethContract, amount: targetCallValue },
];
}
const opportunity: OpportunityParams = {
const opportunity: OpportunityCreate = {
chainId: this.chainId,
targetContract: this.vaultContract,
targetCalldata: calldata,
Expand Down
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-js",
"version": "0.10.0",
"version": "0.11.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
6 changes: 0 additions & 6 deletions express_relay/sdk/js/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ export const OPPORTUNITY_ADAPTER_CONFIGS: Record<

export const SVM_CONSTANTS: Record<string, SvmConstantsConfig> = {
"development-solana": {
relayerSigner: new PublicKey(
"GEeEguHhepHtPVo3E9RA1wvnxgxJ61iSc9dJfd433w3K"
),
feeReceiverRelayer: new PublicKey(
"feesJcX9zwLiEZs9iQGXeBd65b9m2Zc1LjjyHngQF29"
),
expressRelayProgram: new PublicKey(
"PytERJFhAKuNNuaiXkApLfWzwNwSNDACpigT3LwQfou"
),
Expand Down
212 changes: 212 additions & 0 deletions express_relay/sdk/js/src/evm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import {
Bid,
BidParams,
OpportunityBid,
OpportunityEvm,
TokenAmount,
TokenPermissions,
} from "./types";
import { Address, encodeFunctionData, getContractAddress, Hex } from "viem";
import { privateKeyToAccount, signTypedData } from "viem/accounts";
import { checkAddress, ClientError } from "./index";
import { OPPORTUNITY_ADAPTER_CONFIGS } from "./const";
import { executeOpportunityAbi } from "./abi";

/**
* 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;
}

function getOpportunityConfig(chainId: string) {
const opportunityAdapterConfig = OPPORTUNITY_ADAPTER_CONFIGS[chainId];
if (!opportunityAdapterConfig) {
throw new ClientError(
`Opportunity adapter config not found for chain id: ${chainId}`
);
}
return opportunityAdapterConfig;
}

export async function signBid(
opportunity: OpportunityEvm,
bidParams: BidParams,
privateKey: Hex
): Promise<Bid> {
const opportunityAdapterConfig = getOpportunityConfig(opportunity.chainId);
const executor = privateKeyToAccount(privateKey).address;
const permitted = getPermittedTokens(
opportunity.sellTokens,
bidParams.amount,
opportunity.targetCallValue,
checkAddress(opportunityAdapterConfig.weth)
);
const signature = await getSignature(opportunity, bidParams, privateKey);

const calldata = makeAdapterCalldata(
opportunity,
permitted,
executor,
bidParams,
signature
);

return {
amount: bidParams.amount,
targetCalldata: calldata,
chainId: opportunity.chainId,
targetContract: opportunityAdapterConfig.opportunity_adapter_factory,
permissionKey: opportunity.permissionKey,
env: "evm",
};
}

/**
* Constructs the calldata for the opportunity adapter contract.
* @param opportunity Opportunity to bid on
* @param permitted Permitted tokens
* @param executor Address of the searcher's wallet
* @param bidParams Bid amount, nonce, and deadline timestamp
* @param signature Searcher's signature for opportunity params and bidParams
* @returns Calldata for the opportunity adapter contract
*/
function makeAdapterCalldata(
opportunity: OpportunityEvm,
permitted: TokenPermissions[],
executor: Address,
bidParams: BidParams,
signature: Hex
): Hex {
return encodeFunctionData({
abi: [executeOpportunityAbi],
args: [
[
[permitted, bidParams.nonce, bidParams.deadline],
[
opportunity.buyTokens,
executor,
opportunity.targetContract,
opportunity.targetCalldata,
opportunity.targetCallValue,
bidParams.amount,
],
],
signature,
],
});
}

export async function getSignature(
opportunity: OpportunityEvm,
bidParams: BidParams,
privateKey: Hex
): Promise<`0x${string}`> {
const types = {
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: "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 executor = account.address;
const opportunityAdapterConfig = getOpportunityConfig(opportunity.chainId);
const permitted = getPermittedTokens(
opportunity.sellTokens,
bidParams.amount,
opportunity.targetCallValue,
checkAddress(opportunityAdapterConfig.weth)
);
const create2Address = getContractAddress({
bytecodeHash:
opportunityAdapterConfig.opportunity_adapter_init_bytecode_hash,
from: opportunityAdapterConfig.opportunity_adapter_factory,
opcode: "CREATE2",
salt: `0x${executor.replace("0x", "").padStart(64, "0")}`,
});

return signTypedData({
privateKey,
domain: {
name: "Permit2",
verifyingContract: checkAddress(opportunityAdapterConfig.permit2),
chainId: opportunityAdapterConfig.chain_id,
},
types,
primaryType: "PermitBatchWitnessTransferFrom",
message: {
permitted,
spender: create2Address,
nonce: bidParams.nonce,
deadline: bidParams.deadline,
witness: {
buyTokens: opportunity.buyTokens,
executor,
targetContract: opportunity.targetContract,
targetCalldata: opportunity.targetCalldata,
targetCallValue: opportunity.targetCallValue,
bidAmount: bidParams.amount,
},
},
});
}

export async function signOpportunityBid(
opportunity: OpportunityEvm,
bidParams: BidParams,
privateKey: Hex
): Promise<OpportunityBid> {
const account = privateKeyToAccount(privateKey);
const signature = await getSignature(opportunity, bidParams, privateKey);

return {
permissionKey: opportunity.permissionKey,
bid: bidParams,
executor: account.address,
signature,
opportunityId: opportunity.opportunityId,
};
}
2 changes: 2 additions & 0 deletions express_relay/sdk/js/src/examples/simpleSearcherEvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class SimpleSearcherEvm {
}

async opportunityHandler(opportunity: Opportunity) {
if (!("targetContract" in opportunity))
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we trying to validate fields and type here or just checking if it's a Svm or Evm opp?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

just checking it's an Svm/Evm opp.

throw new Error("Not a valid EVM opportunity");
const bidAmount = 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
Expand Down
Loading
Loading