diff --git a/apps/hermes/client/js/package.json b/apps/hermes/client/js/package.json index 98073c5d2..b8cfa6177 100644 --- a/apps/hermes/client/js/package.json +++ b/apps/hermes/client/js/package.json @@ -36,6 +36,7 @@ ], "license": "Apache-2.0", "devDependencies": { + "@types/bn.js": "^5.1.5", "@types/eventsource": "^1.1.15", "@types/jest": "^29.4.0", "@types/node": "^20.14.2", @@ -52,6 +53,7 @@ }, "dependencies": { "@zodios/core": "^10.9.6", + "bn.js": "^5.2.1", "eventsource": "^2.0.2", "zod": "^3.23.8" } diff --git a/apps/hermes/client/js/src/AccumulatorUpdateData.ts b/apps/hermes/client/js/src/AccumulatorUpdateData.ts new file mode 100644 index 000000000..b57132bed --- /dev/null +++ b/apps/hermes/client/js/src/AccumulatorUpdateData.ts @@ -0,0 +1,215 @@ +import BN from "bn.js"; + +const ACCUMULATOR_MAGIC = "504e4155"; +const MAJOR_VERSION = 1; +const MINOR_VERSION = 0; +const KECCAK160_HASH_SIZE = 20; +const PRICE_FEED_MESSAGE_VARIANT = 0; +const TWAP_MESSAGE_VARIANT = 1; + +export type AccumulatorUpdateData = { + vaa: Buffer; + updates: { message: Buffer; proof: number[][] }[]; +}; +export type PriceFeedMessage = { + feedId: Buffer; + price: BN; + confidence: BN; + exponent: number; + publishTime: BN; + prevPublishTime: BN; + emaPrice: BN; + emaConf: BN; +}; + +export type TwapMessage = { + feedId: Buffer; + cumulativePrice: BN; + cumulativeConf: BN; + numDownSlots: BN; + exponent: number; + publishTime: BN; + prevPublishTime: BN; + publishSlot: BN; +}; + +export function isAccumulatorUpdateData(updateBytes: Buffer): boolean { + return ( + updateBytes.toString("hex").slice(0, 8) === ACCUMULATOR_MAGIC && + updateBytes[4] === MAJOR_VERSION && + updateBytes[5] === MINOR_VERSION + ); +} + +export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage { + let cursor = 0; + const variant = message.readUInt8(cursor); + if (variant !== PRICE_FEED_MESSAGE_VARIANT) { + throw new Error("Not a price feed message"); + } + cursor += 1; + const feedId = message.subarray(cursor, cursor + 32); + cursor += 32; + const price = new BN(message.subarray(cursor, cursor + 8), "be"); + cursor += 8; + const confidence = new BN(message.subarray(cursor, cursor + 8), "be"); + cursor += 8; + const exponent = message.readInt32BE(cursor); + cursor += 4; + const publishTime = new BN(message.subarray(cursor, cursor + 8), "be"); + cursor += 8; + const prevPublishTime = new BN(message.subarray(cursor, cursor + 8), "be"); + cursor += 8; + const emaPrice = new BN(message.subarray(cursor, cursor + 8), "be"); + cursor += 8; + const emaConf = new BN(message.subarray(cursor, cursor + 8), "be"); + cursor += 8; + return { + feedId, + price, + confidence, + exponent, + publishTime, + prevPublishTime, + emaPrice, + emaConf, + }; +} + +export function parseTwapMessage(message: Buffer): TwapMessage { + let cursor = 0; + const variant = message.readUInt8(cursor); + if (variant !== TWAP_MESSAGE_VARIANT) { + throw new Error("Not a twap message"); + } + cursor += 1; + const feedId = message.subarray(cursor, cursor + 32); + cursor += 32; + const cumulativePrice = new BN(message.subarray(cursor, cursor + 16), "be"); + cursor += 16; + const cumulativeConf = new BN(message.subarray(cursor, cursor + 16), "be"); + cursor += 16; + const numDownSlots = new BN(message.subarray(cursor, cursor + 8), "be"); + cursor += 8; + const exponent = message.readInt32BE(cursor); + cursor += 4; + const publishTime = new BN(message.subarray(cursor, cursor + 8), "be"); + cursor += 8; + const prevPublishTime = new BN(message.subarray(cursor, cursor + 8), "be"); + cursor += 8; + const publishSlot = new BN(message.subarray(cursor, cursor + 8), "be"); + cursor += 8; + return { + feedId, + cumulativePrice, + cumulativeConf, + numDownSlots, + exponent, + publishTime, + prevPublishTime, + publishSlot, + }; +} + +/** + * An AccumulatorUpdateData contains a VAA and a list of updates. This function returns a new serialized AccumulatorUpdateData with only the updates in the range [start, end). + */ +export function sliceAccumulatorUpdateData( + data: Buffer, + start?: number, + end?: number +): Buffer { + if (!isAccumulatorUpdateData(data)) { + throw new Error("Invalid accumulator message"); + } + let cursor = 6; + const trailingPayloadSize = data.readUint8(cursor); + cursor += 1 + trailingPayloadSize; + + // const proofType = data.readUint8(cursor); + cursor += 1; + + const vaaSize = data.readUint16BE(cursor); + cursor += 2; + cursor += vaaSize; + + const endOfVaa = cursor; + + const updates = []; + const numUpdates = data.readUInt8(cursor); + cursor += 1; + + for (let i = 0; i < numUpdates; i++) { + const updateStart = cursor; + const messageSize = data.readUint16BE(cursor); + cursor += 2; + cursor += messageSize; + + const numProofs = data.readUInt8(cursor); + cursor += 1; + cursor += KECCAK160_HASH_SIZE * numProofs; + + updates.push(data.subarray(updateStart, cursor)); + } + + if (cursor !== data.length) { + throw new Error("Didn't reach the end of the message"); + } + + const sliceUpdates = updates.slice(start, end); + return Buffer.concat([ + data.subarray(0, endOfVaa), + Buffer.from([sliceUpdates.length]), + ...updates.slice(start, end), + ]); +} + +export function parseAccumulatorUpdateData( + data: Buffer +): AccumulatorUpdateData { + if (!isAccumulatorUpdateData(data)) { + throw new Error("Invalid accumulator message"); + } + + let cursor = 6; + const trailingPayloadSize = data.readUint8(cursor); + cursor += 1 + trailingPayloadSize; + + // const proofType = data.readUint8(cursor); + cursor += 1; + + const vaaSize = data.readUint16BE(cursor); + cursor += 2; + + const vaa = data.subarray(cursor, cursor + vaaSize); + cursor += vaaSize; + + const numUpdates = data.readUInt8(cursor); + const updates = []; + cursor += 1; + + for (let i = 0; i < numUpdates; i++) { + const messageSize = data.readUint16BE(cursor); + cursor += 2; + const message = data.subarray(cursor, cursor + messageSize); + cursor += messageSize; + + const numProofs = data.readUInt8(cursor); + cursor += 1; + const proof = []; + for (let j = 0; j < numProofs; j++) { + proof.push( + Array.from(data.subarray(cursor, cursor + KECCAK160_HASH_SIZE)) + ); + cursor += KECCAK160_HASH_SIZE; + } + + updates.push({ message, proof }); + } + + if (cursor !== data.length) { + throw new Error("Didn't reach the end of the message"); + } + + return { vaa, updates }; +} diff --git a/apps/hermes/client/js/src/__tests__/AccumulatorUpdateData.test.ts b/apps/hermes/client/js/src/__tests__/AccumulatorUpdateData.test.ts new file mode 100644 index 000000000..53707b053 --- /dev/null +++ b/apps/hermes/client/js/src/__tests__/AccumulatorUpdateData.test.ts @@ -0,0 +1,151 @@ +import { + parseAccumulatorUpdateData, + parsePriceFeedMessage, + parseTwapMessage, + sliceAccumulatorUpdateData, +} from "../AccumulatorUpdateData"; + +// This is just a sample update data from hermes +const TEST_ACCUMULATOR_UPDATE_DATA = + "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"; + +describe("Test parse accumulator update", () => { + test("Happy path", async () => { + const { vaa, updates } = parseAccumulatorUpdateData( + Buffer.from(TEST_ACCUMULATOR_UPDATE_DATA, "base64") + ); + + const priceMessages = updates.map((update) => { + return parsePriceFeedMessage(update.message); + }); + expect(priceMessages[0].feedId.toString("hex")).toBe( + "c96458d393fe9deb7a7d63a0ac41e2898a67a7750dbd166673279e06c868df0a" + ); + expect(priceMessages[0].price.toString()).toBe("4689500"); + expect(priceMessages[0].confidence.toString()).toBe("6174"); + expect(priceMessages[0].exponent).toBe(-8); + expect(priceMessages[0].publishTime.toString()).toBe("1715627146"); + expect(priceMessages[0].prevPublishTime.toString()).toBe("1715627145"); + expect(priceMessages[0].emaPrice.toString()).toBe("4690654"); + expect(priceMessages[0].emaConf.toString()).toBe("6257"); + + expect(priceMessages[1].feedId.toString("hex")).toBe( + "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace" + ); + expect(priceMessages[1].price.toString()).toBe("294943041422"); + expect(priceMessages[1].confidence.toString()).toBe("184284043"); + expect(priceMessages[1].exponent).toBe(-8); + expect(priceMessages[1].publishTime.toString()).toBe("1715627146"); + expect(priceMessages[1].prevPublishTime.toString()).toBe("1715627145"); + expect(priceMessages[1].emaPrice.toString()).toBe("294580230000"); + expect(priceMessages[1].emaConf.toString()).toBe("239495968"); + + expect(priceMessages[2].feedId.toString("hex")).toBe( + "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d" + ); + expect(priceMessages[2].price.toString()).toBe("14802549538"); + expect(priceMessages[2].confidence.toString()).toBe("9930888"); + expect(priceMessages[2].exponent).toBe(-8); + expect(priceMessages[2].publishTime.toString()).toBe("1715627146"); + expect(priceMessages[2].prevPublishTime.toString()).toBe("1715627145"); + expect(priceMessages[2].emaPrice.toString()).toBe("14732272500"); + expect(priceMessages[2].emaConf.toString()).toBe("10975810"); + }); + + test("Slice accumulator update data", async () => { + expect( + parseAccumulatorUpdateData( + sliceAccumulatorUpdateData( + Buffer.from(TEST_ACCUMULATOR_UPDATE_DATA, "base64"), + 2, + 1 + ) + ).updates.length + ).toBe(0); + + expect( + parseAccumulatorUpdateData( + sliceAccumulatorUpdateData( + Buffer.from(TEST_ACCUMULATOR_UPDATE_DATA, "base64"), + 0, + 5 + ) + ).updates.length + ).toBe(3); + + const { vaa, updates } = parseAccumulatorUpdateData( + sliceAccumulatorUpdateData( + Buffer.from(TEST_ACCUMULATOR_UPDATE_DATA, "base64"), + 1, + 3 + ) + ); + + const priceMessages = updates.map((update) => { + return parsePriceFeedMessage(update.message); + }); + expect(priceMessages[0].feedId.toString("hex")).toBe( + "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace" + ); + expect(priceMessages[0].price.toString()).toBe("294943041422"); + expect(priceMessages[0].confidence.toString()).toBe("184284043"); + expect(priceMessages[0].exponent).toBe(-8); + expect(priceMessages[0].publishTime.toString()).toBe("1715627146"); + expect(priceMessages[0].prevPublishTime.toString()).toBe("1715627145"); + expect(priceMessages[0].emaPrice.toString()).toBe("294580230000"); + expect(priceMessages[0].emaConf.toString()).toBe("239495968"); + + expect(priceMessages[1].feedId.toString("hex")).toBe( + "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d" + ); + expect(priceMessages[1].price.toString()).toBe("14802549538"); + expect(priceMessages[1].confidence.toString()).toBe("9930888"); + expect(priceMessages[1].exponent).toBe(-8); + expect(priceMessages[1].publishTime.toString()).toBe("1715627146"); + expect(priceMessages[1].prevPublishTime.toString()).toBe("1715627145"); + expect(priceMessages[1].emaPrice.toString()).toBe("14732272500"); + expect(priceMessages[1].emaConf.toString()).toBe("10975810"); + }); + + test("Wrong magic number", async () => { + const data = Buffer.from(TEST_ACCUMULATOR_UPDATE_DATA, "base64"); + data[0] = 0; + expect(() => parseAccumulatorUpdateData(data)).toThrow( + "Invalid accumulator message" + ); + }); + + test("Parse TWAP message", () => { + // Sample data from the Hermes latest TWAP endpoint. + const testAccumulatorDataTwap = + "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"; + const { updates } = parseAccumulatorUpdateData( + Buffer.from(testAccumulatorDataTwap, "base64") + ); + + // Test that both messages are parsed successfully + const twapMessage1 = parseTwapMessage(updates[0].message); + expect(twapMessage1.feedId.toString("hex")).toBe( + "49f6b65cb1de6b10eaf75e7c03ca029c306d0357e91b5311b175084a5ad55688" + ); + expect(twapMessage1.cumulativePrice.toString()).toBe("1760238576144013"); + expect(twapMessage1.cumulativeConf.toString()).toBe("5113466755162"); + expect(twapMessage1.numDownSlots.toString()).toBe("72037403"); + expect(twapMessage1.exponent).toBe(-5); + expect(twapMessage1.publishTime.toString()).toBe("1733155135"); + expect(twapMessage1.prevPublishTime.toString()).toBe("1733155134"); + expect(twapMessage1.publishSlot.toString()).toBe("181871343"); + + const twapMessage2 = parseTwapMessage(updates[1].message); + expect(twapMessage2.feedId.toString("hex")).toBe( + "2b9ab1e972a281585084148ba1389800799bd4be63b957507db1349314e47445" + ); + expect(twapMessage2.cumulativePrice.toString()).toBe("949830028892149802"); + expect(twapMessage2.cumulativeConf.toString()).toBe("973071467813959"); + expect(twapMessage2.numDownSlots.toString()).toBe("27140"); + expect(twapMessage2.exponent).toBe(-8); + expect(twapMessage2.publishTime.toString()).toBe("1733155135"); + expect(twapMessage2.prevPublishTime.toString()).toBe("1733155134"); + expect(twapMessage2.publishSlot.toString()).toBe("181871343"); + }); +}); diff --git a/apps/hermes/client/js/src/index.ts b/apps/hermes/client/js/src/index.ts new file mode 100644 index 000000000..5ca4df21d --- /dev/null +++ b/apps/hermes/client/js/src/index.ts @@ -0,0 +1,14 @@ +export { + isAccumulatorUpdateData, + sliceAccumulatorUpdateData, + parseAccumulatorUpdateData, + AccumulatorUpdateData, + parsePriceFeedMessage, + parseTwapMessage, + PriceFeedMessage, + TwapMessage, +} from "./AccumulatorUpdateData"; + +export * from "./HermesClient"; +export * from "./zodSchemas"; +export * from "./utils"; diff --git a/express_relay/sdk/js/lib/abi.d.ts b/express_relay/sdk/js/lib/abi.d.ts new file mode 100644 index 000000000..5dc3b32ae --- /dev/null +++ b/express_relay/sdk/js/lib/abi.d.ts @@ -0,0 +1,37 @@ +export declare const executeOpportunityAbi: { + type: string; + name: string; + inputs: ({ + name: string; + type: string; + internalType: string; + components: { + name: string; + type: string; + internalType: string; + components: ({ + name: string; + type: string; + internalType: string; + components: { + name: string; + type: string; + internalType: string; + }[]; + } | { + name: string; + type: string; + internalType: string; + components?: undefined; + })[]; + }[]; + } | { + name: string; + type: string; + internalType: string; + components?: undefined; + })[]; + outputs: never[]; + stateMutability: string; +}; +//# sourceMappingURL=abi.d.ts.map \ No newline at end of file diff --git a/express_relay/sdk/js/lib/abi.d.ts.map b/express_relay/sdk/js/lib/abi.d.ts.map new file mode 100644 index 000000000..9cddbb2e9 --- /dev/null +++ b/express_relay/sdk/js/lib/abi.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"abi.d.ts","sourceRoot":"","sources":["../src/abi.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsGjC,CAAC"} \ No newline at end of file diff --git a/express_relay/sdk/js/lib/abi.js b/express_relay/sdk/js/lib/abi.js new file mode 100644 index 000000000..c304e3161 --- /dev/null +++ b/express_relay/sdk/js/lib/abi.js @@ -0,0 +1,106 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.executeOpportunityAbi = void 0; +exports.executeOpportunityAbi = { + type: "function", + name: "executeOpportunity", + inputs: [ + { + name: "params", + type: "tuple", + internalType: "struct ExecutionParams", + components: [ + { + name: "permit", + type: "tuple", + internalType: "struct ISignatureTransfer.PermitBatchTransferFrom", + components: [ + { + name: "permitted", + type: "tuple[]", + internalType: "struct ISignatureTransfer.TokenPermissions[]", + components: [ + { + name: "token", + type: "address", + internalType: "address", + }, + { + name: "amount", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256", + }, + { + name: "deadline", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + name: "witness", + type: "tuple", + internalType: "struct ExecutionWitness", + components: [ + { + name: "buyTokens", + type: "tuple[]", + internalType: "struct TokenAmount[]", + components: [ + { + name: "token", + type: "address", + internalType: "address", + }, + { + name: "amount", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + name: "executor", + type: "address", + internalType: "address", + }, + { + name: "targetContract", + type: "address", + internalType: "address", + }, + { + name: "targetCalldata", + type: "bytes", + internalType: "bytes", + }, + { + name: "targetCallValue", + type: "uint256", + internalType: "uint256", + }, + { + name: "bidAmount", + type: "uint256", + internalType: "uint256", + }, + ], + }, + ], + }, + { + name: "signature", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [], + stateMutability: "payable", +}; diff --git a/express_relay/sdk/js/lib/const.d.ts b/express_relay/sdk/js/lib/const.d.ts new file mode 100644 index 000000000..3f67505c9 --- /dev/null +++ b/express_relay/sdk/js/lib/const.d.ts @@ -0,0 +1,4 @@ +import { OpportunityAdapterConfig, SvmConstantsConfig } from "./types"; +export declare const OPPORTUNITY_ADAPTER_CONFIGS: Record; +export declare const SVM_CONSTANTS: Record; +//# sourceMappingURL=const.d.ts.map \ No newline at end of file diff --git a/express_relay/sdk/js/lib/const.d.ts.map b/express_relay/sdk/js/lib/const.d.ts.map new file mode 100644 index 000000000..a72a5f052 --- /dev/null +++ b/express_relay/sdk/js/lib/const.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../src/const.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEvE,eAAO,MAAM,2BAA2B,EAAE,MAAM,CAC9C,MAAM,EACN,wBAAwB,CAkBzB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAM5D,CAAC"} \ No newline at end of file diff --git a/express_relay/sdk/js/lib/const.js b/express_relay/sdk/js/lib/const.js new file mode 100644 index 000000000..d385a368b --- /dev/null +++ b/express_relay/sdk/js/lib/const.js @@ -0,0 +1,25 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SVM_CONSTANTS = exports.OPPORTUNITY_ADAPTER_CONFIGS = void 0; +const web3_js_1 = require("@solana/web3.js"); +exports.OPPORTUNITY_ADAPTER_CONFIGS = { + op_sepolia: { + chain_id: 11155420, + opportunity_adapter_factory: "0xfA119693864b2F185742A409c66f04865c787754", + opportunity_adapter_init_bytecode_hash: "0x3d71516d94b96a8fdca4e3a5825a6b41c9268a8e94610367e69a8462cc543533", + permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + weth: "0x74A4A85C611679B73F402B36c0F84A7D2CcdFDa3", + }, + mode: { + chain_id: 34443, + opportunity_adapter_factory: "0x59F78DE21a0b05d96Ae00c547BA951a3B905602f", + opportunity_adapter_init_bytecode_hash: "0xd53b8e32ab2ecba07c3e3a17c3c5e492c62e2f7051b89e5154f52e6bfeb0e38f", + permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + weth: "0x4200000000000000000000000000000000000006", + }, +}; +exports.SVM_CONSTANTS = { + "development-solana": { + expressRelayProgram: new web3_js_1.PublicKey("PytERJFhAKuNNuaiXkApLfWzwNwSNDACpigT3LwQfou"), + }, +}; diff --git a/express_relay/sdk/js/lib/evm.d.ts b/express_relay/sdk/js/lib/evm.d.ts new file mode 100644 index 000000000..08ba24897 --- /dev/null +++ b/express_relay/sdk/js/lib/evm.d.ts @@ -0,0 +1,6 @@ +import { Bid, BidParams, OpportunityBid, OpportunityEvm } from "./types"; +import { Hex } from "viem"; +export declare function signBid(opportunity: OpportunityEvm, bidParams: BidParams, privateKey: Hex): Promise; +export declare function getSignature(opportunity: OpportunityEvm, bidParams: BidParams, privateKey: Hex): Promise<`0x${string}`>; +export declare function signOpportunityBid(opportunity: OpportunityEvm, bidParams: BidParams, privateKey: Hex): Promise; +//# sourceMappingURL=evm.d.ts.map \ No newline at end of file diff --git a/express_relay/sdk/js/lib/evm.d.ts.map b/express_relay/sdk/js/lib/evm.d.ts.map new file mode 100644 index 000000000..367ecbc06 --- /dev/null +++ b/express_relay/sdk/js/lib/evm.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"evm.d.ts","sourceRoot":"","sources":["../src/evm.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EACH,SAAS,EACT,cAAc,EACd,cAAc,EAGf,MAAM,SAAS,CAAC;AACjB,OAAO,EAAmD,GAAG,EAAE,MAAM,MAAM,CAAC;AA8C5E,wBAAsB,OAAO,CAC3B,WAAW,EAAE,cAAc,EAC3B,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,GAAG,GACd,OAAO,CAAC,GAAG,CAAC,CA2Bd;AAqCD,wBAAsB,YAAY,CAChC,WAAW,EAAE,cAAc,EAC3B,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,GAAG,GACd,OAAO,CAAC,KAAK,MAAM,EAAE,CAAC,CAoExB;AAED,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,cAAc,EAC3B,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,GAAG,GACd,OAAO,CAAC,cAAc,CAAC,CAWzB"} \ No newline at end of file diff --git a/express_relay/sdk/js/lib/evm.js b/express_relay/sdk/js/lib/evm.js new file mode 100644 index 000000000..10546af2a --- /dev/null +++ b/express_relay/sdk/js/lib/evm.js @@ -0,0 +1,156 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.signBid = signBid; +exports.getSignature = getSignature; +exports.signOpportunityBid = signOpportunityBid; +const viem_1 = require("viem"); +const accounts_1 = require("viem/accounts"); +const index_1 = require("./index"); +const const_1 = require("./const"); +const abi_1 = require("./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, bidAmount, callValue, weth) { + const permitted = 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) { + const opportunityAdapterConfig = const_1.OPPORTUNITY_ADAPTER_CONFIGS[chainId]; + if (!opportunityAdapterConfig) { + throw new index_1.ClientError(`Opportunity adapter config not found for chain id: ${chainId}`); + } + return opportunityAdapterConfig; +} +async function signBid(opportunity, bidParams, privateKey) { + const opportunityAdapterConfig = getOpportunityConfig(opportunity.chainId); + const executor = (0, accounts_1.privateKeyToAccount)(privateKey).address; + const permitted = getPermittedTokens(opportunity.sellTokens, bidParams.amount, opportunity.targetCallValue, (0, index_1.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, permitted, executor, bidParams, signature) { + return (0, viem_1.encodeFunctionData)({ + abi: [abi_1.executeOpportunityAbi], + args: [ + [ + [permitted, bidParams.nonce, bidParams.deadline], + [ + opportunity.buyTokens, + executor, + opportunity.targetContract, + opportunity.targetCalldata, + opportunity.targetCallValue, + bidParams.amount, + ], + ], + signature, + ], + }); +} +async function getSignature(opportunity, bidParams, privateKey) { + 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 = (0, accounts_1.privateKeyToAccount)(privateKey); + const executor = account.address; + const opportunityAdapterConfig = getOpportunityConfig(opportunity.chainId); + const permitted = getPermittedTokens(opportunity.sellTokens, bidParams.amount, opportunity.targetCallValue, (0, index_1.checkAddress)(opportunityAdapterConfig.weth)); + const create2Address = (0, viem_1.getContractAddress)({ + bytecodeHash: opportunityAdapterConfig.opportunity_adapter_init_bytecode_hash, + from: opportunityAdapterConfig.opportunity_adapter_factory, + opcode: "CREATE2", + salt: `0x${executor.replace("0x", "").padStart(64, "0")}`, + }); + return (0, accounts_1.signTypedData)({ + privateKey, + domain: { + name: "Permit2", + verifyingContract: (0, index_1.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, + }, + }, + }); +} +async function signOpportunityBid(opportunity, bidParams, privateKey) { + const account = (0, accounts_1.privateKeyToAccount)(privateKey); + const signature = await getSignature(opportunity, bidParams, privateKey); + return { + permissionKey: opportunity.permissionKey, + bid: bidParams, + executor: account.address, + signature, + opportunityId: opportunity.opportunityId, + }; +} diff --git a/express_relay/sdk/js/lib/examples/idl/idlDummy.json b/express_relay/sdk/js/lib/examples/idl/idlDummy.json new file mode 100644 index 000000000..bfad9f3d3 --- /dev/null +++ b/express_relay/sdk/js/lib/examples/idl/idlDummy.json @@ -0,0 +1,141 @@ +{ + "address": "HYCgALnu6CM2gkQVopa1HGaNf8Vzbs9bomWRiKP267P3", + "metadata": { + "name": "dummy", + "version": "0.2.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "do_nothing", + "discriminator": [112, 130, 224, 161, 71, 149, 192, 187], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "express_relay", + "address": "GwEtasTAxdS9neVE4GPUpcwR7DB7AizntQSPcG36ubZM" + }, + { + "name": "express_relay_metadata", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + } + ], + "program": { + "kind": "account", + "path": "express_relay" + } + } + }, + { + "name": "sysvar_instructions", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "permission" + }, + { + "name": "router" + }, + { + "name": "config_router", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 111, 110, 102, 105, 103, 95, 114, 111, 117, 116, 101, 114 + ] + }, + { + "kind": "account", + "path": "router" + } + ], + "program": { + "kind": "account", + "path": "express_relay" + } + } + }, + { + "name": "accounting", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [97, 99, 99, 111, 117, 110, 116, 105, 110, 103] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "Accounting", + "discriminator": [1, 249, 15, 214, 81, 88, 40, 108] + }, + { + "name": "ExpressRelayMetadata", + "discriminator": [204, 75, 133, 7, 175, 241, 130, 11] + } + ], + "types": [ + { + "name": "Accounting", + "type": { + "kind": "struct", + "fields": [ + { + "name": "total_fees", + "type": "u64" + } + ] + } + }, + { + "name": "ExpressRelayMetadata", + "type": { + "kind": "struct", + "fields": [ + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "relayer_signer", + "type": "pubkey" + }, + { + "name": "fee_receiver_relayer", + "type": "pubkey" + }, + { + "name": "split_router_default", + "type": "u64" + }, + { + "name": "split_relayer", + "type": "u64" + } + ] + } + } + ] +} diff --git a/express_relay/sdk/js/lib/examples/simpleSearcherEvm.d.ts b/express_relay/sdk/js/lib/examples/simpleSearcherEvm.d.ts new file mode 100644 index 000000000..e31840f03 --- /dev/null +++ b/express_relay/sdk/js/lib/examples/simpleSearcherEvm.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=simpleSearcherEvm.d.ts.map \ No newline at end of file diff --git a/express_relay/sdk/js/lib/examples/simpleSearcherEvm.d.ts.map b/express_relay/sdk/js/lib/examples/simpleSearcherEvm.d.ts.map new file mode 100644 index 000000000..6c867275c --- /dev/null +++ b/express_relay/sdk/js/lib/examples/simpleSearcherEvm.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"simpleSearcherEvm.d.ts","sourceRoot":"","sources":["../../src/examples/simpleSearcherEvm.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/express_relay/sdk/js/lib/examples/simpleSearcherEvm.js b/express_relay/sdk/js/lib/examples/simpleSearcherEvm.js new file mode 100644 index 000000000..49e765f0b --- /dev/null +++ b/express_relay/sdk/js/lib/examples/simpleSearcherEvm.js @@ -0,0 +1,121 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const yargs_1 = __importDefault(require("yargs")); +const helpers_1 = require("yargs/helpers"); +const index_1 = require("../index"); +const accounts_1 = require("viem/accounts"); +const viem_1 = require("viem"); +const const_1 = require("../const"); +const DAY_IN_SECONDS = 60 * 60 * 24; +class SimpleSearcherEvm { + endpoint; + chainId; + privateKey; + apiKey; + client; + constructor(endpoint, chainId, privateKey, apiKey) { + this.endpoint = endpoint; + this.chainId = chainId; + this.privateKey = privateKey; + this.apiKey = apiKey; + this.client = new index_1.Client({ + baseUrl: endpoint, + apiKey, + }, undefined, this.opportunityHandler.bind(this), this.bidStatusHandler.bind(this)); + } + async bidStatusHandler(_bidStatus) { + const bidStatus = _bidStatus; + let resultDetails = ""; + if (bidStatus.type == "submitted" || bidStatus.type == "won") { + resultDetails = `, transaction ${bidStatus.result}, index ${bidStatus.index} of multicall`; + } + else if (bidStatus.type == "lost") { + if (bidStatus.result) { + resultDetails = `, transaction ${bidStatus.result}`; + } + if (bidStatus.index) { + resultDetails += `, index ${bidStatus.index} of multicall`; + } + } + console.log(`Bid status for bid ${bidStatus.id}: ${bidStatus.type}${resultDetails}`); + } + async opportunityHandler(opportunity) { + if (!("targetContract" in opportunity)) + 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 + // TODO: generate nonce more intelligently, to reduce gas costs + const nonce = BigInt(Math.floor(Math.random() * 2 ** 50)); + const bidParams = { + amount: bidAmount, + nonce: nonce, + deadline: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), + }; + const bid = await this.client.signBid(opportunity, bidParams, (0, index_1.checkHex)(argv.privateKey)); + try { + const bidId = await this.client.submitBid(bid); + console.log(`Successful bid. Opportunity id ${opportunity.opportunityId} Bid id ${bidId}`); + } + catch (error) { + console.error(`Failed to bid on opportunity ${opportunity.opportunityId}: ${error}`); + } + } + async start() { + try { + await this.client.subscribeChains([argv.chainId]); + console.log(`Subscribed to chain ${argv.chainId}. Waiting for opportunities...`); + } + catch (error) { + console.error(error); + this.client.websocket?.close(); + } + } +} +const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) + .option("endpoint", { + description: "Express relay endpoint. e.g: https://per-staging.dourolabs.app/", + type: "string", + demandOption: true, +}) + .option("chain-id", { + description: "Chain id to fetch opportunities for. e.g: sepolia", + type: "string", + demandOption: true, +}) + .option("bid", { + description: "Bid amount in wei", + type: "string", + default: "10000000000000000", +}) + .option("private-key", { + description: "Private key to sign the bid with in hex format with 0x prefix. e.g: 0xdeadbeef...", + type: "string", + demandOption: true, +}) + .option("api-key", { + description: "The API key of the searcher to authenticate with the server for fetching and submitting bids", + type: "string", + demandOption: false, +}) + .help() + .alias("help", "h") + .parseSync(); +async function run() { + if ((0, viem_1.isHex)(argv.privateKey)) { + const account = (0, accounts_1.privateKeyToAccount)(argv.privateKey); + console.log(`Using account: ${account.address}`); + } + else { + throw new Error(`Invalid private key: ${argv.privateKey}`); + } + const searcher = new SimpleSearcherEvm(argv.endpoint, argv.chainId, argv.privateKey, argv.apiKey); + if (const_1.OPPORTUNITY_ADAPTER_CONFIGS[argv.chainId] === undefined) { + throw new Error(`Opportunity adapter config not found for chain ${argv.chainId}`); + } + await searcher.start(); +} +run(); diff --git a/express_relay/sdk/js/lib/examples/simpleSearcherLimo.d.ts b/express_relay/sdk/js/lib/examples/simpleSearcherLimo.d.ts new file mode 100644 index 000000000..aaad50d48 --- /dev/null +++ b/express_relay/sdk/js/lib/examples/simpleSearcherLimo.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=simpleSearcherLimo.d.ts.map \ No newline at end of file diff --git a/express_relay/sdk/js/lib/examples/simpleSearcherLimo.d.ts.map b/express_relay/sdk/js/lib/examples/simpleSearcherLimo.d.ts.map new file mode 100644 index 000000000..1b1350465 --- /dev/null +++ b/express_relay/sdk/js/lib/examples/simpleSearcherLimo.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"simpleSearcherLimo.d.ts","sourceRoot":"","sources":["../../src/examples/simpleSearcherLimo.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/express_relay/sdk/js/lib/examples/simpleSearcherLimo.js b/express_relay/sdk/js/lib/examples/simpleSearcherLimo.js new file mode 100644 index 000000000..593f24d60 --- /dev/null +++ b/express_relay/sdk/js/lib/examples/simpleSearcherLimo.js @@ -0,0 +1,201 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const yargs_1 = __importDefault(require("yargs")); +const helpers_1 = require("yargs/helpers"); +const index_1 = require("../index"); +const const_1 = require("../const"); +const anchor = __importStar(require("@coral-xyz/anchor")); +const web3_js_1 = require("@solana/web3.js"); +const limo = __importStar(require("@kamino-finance/limo-sdk")); +const decimal_js_1 = require("decimal.js"); +const utils_1 = require("@kamino-finance/limo-sdk/dist/utils"); +const DAY_IN_SECONDS = 60 * 60 * 24; +class SimpleSearcherLimo { + endpointExpressRelay; + chainId; + searcher; + endpointSvm; + fillRate; + apiKey; + client; + connectionSvm; + mintDecimals = {}; + expressRelayConfig; + constructor(endpointExpressRelay, chainId, searcher, endpointSvm, fillRate, apiKey) { + this.endpointExpressRelay = endpointExpressRelay; + this.chainId = chainId; + this.searcher = searcher; + this.endpointSvm = endpointSvm; + this.fillRate = fillRate; + this.apiKey = apiKey; + this.client = new index_1.Client({ + baseUrl: endpointExpressRelay, + apiKey, + }, undefined, this.opportunityHandler.bind(this), this.bidStatusHandler.bind(this)); + this.connectionSvm = new web3_js_1.Connection(endpointSvm, "confirmed"); + } + async bidStatusHandler(bidStatus) { + let resultDetails = ""; + if (bidStatus.type == "submitted" || bidStatus.type == "won") { + resultDetails = `, transaction ${bidStatus.result}`; + } + else if (bidStatus.type == "lost") { + if (bidStatus.result) { + resultDetails = `, transaction ${bidStatus.result}`; + } + } + console.log(`Bid status for bid ${bidStatus.id}: ${bidStatus.type}${resultDetails}`); + } + async getMintDecimalsCached(mint) { + const mintAddress = mint.toBase58(); + if (this.mintDecimals[mintAddress]) { + return this.mintDecimals[mintAddress]; + } + const decimals = await (0, utils_1.getMintDecimals)(this.connectionSvm, mint); + this.mintDecimals[mintAddress] = decimals; + return decimals; + } + async generateBid(opportunity) { + const order = opportunity.order; + const limoClient = new limo.LimoClient(this.connectionSvm, order.state.globalConfig); + const inputMintDecimals = await this.getMintDecimalsCached(order.state.inputMint); + const outputMintDecimals = await this.getMintDecimalsCached(order.state.outputMint); + const effectiveFillRate = Math.min(this.fillRate, (100 * order.state.remainingInputAmount.toNumber()) / + order.state.initialInputAmount.toNumber()); + const inputAmountDecimals = new decimal_js_1.Decimal(order.state.initialInputAmount.toNumber()) + .div(new decimal_js_1.Decimal(10).pow(inputMintDecimals)) + .mul(effectiveFillRate) + .div(100); + const outputAmountDecimals = new decimal_js_1.Decimal(order.state.expectedOutputAmount.toNumber()) + .div(new decimal_js_1.Decimal(10).pow(outputMintDecimals)) + .mul(effectiveFillRate) + .div(100); + console.log("Order address", order.address.toBase58()); + console.log("Fill rate", effectiveFillRate); + console.log("Sell token", order.state.inputMint.toBase58(), "amount:", inputAmountDecimals.toString()); + console.log("Buy token", order.state.outputMint.toBase58(), "amount:", outputAmountDecimals.toString()); + const ixsTakeOrder = await limoClient.takeOrderIx(this.searcher.publicKey, order, inputAmountDecimals, outputAmountDecimals, const_1.SVM_CONSTANTS[this.chainId].expressRelayProgram, inputMintDecimals, outputMintDecimals); + const txRaw = new anchor.web3.Transaction().add(...ixsTakeOrder); + const router = (0, utils_1.getPdaAuthority)(limoClient.getProgramID(), order.state.globalConfig); + const bidAmount = new anchor.BN(argv.bid); + if (!this.expressRelayConfig) { + this.expressRelayConfig = await this.client.getExpressRelaySvmConfig(this.chainId, this.connectionSvm); + } + const bid = await this.client.constructSvmBid(txRaw, this.searcher.publicKey, router, order.address, bidAmount, new anchor.BN(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), this.chainId, this.expressRelayConfig.relayerSigner, this.expressRelayConfig.feeReceiverRelayer); + bid.transaction.recentBlockhash = opportunity.blockHash; + bid.transaction.sign(this.searcher); + return bid; + } + async opportunityHandler(opportunity) { + const bid = await this.generateBid(opportunity); + try { + const bidId = await this.client.submitBid(bid); + console.log(`Successful bid. Opportunity id ${opportunity.opportunityId} Bid id ${bidId}`); + } + catch (error) { + console.error(`Failed to bid on opportunity ${opportunity.opportunityId}: ${error}`); + } + } + async start() { + try { + await this.client.subscribeChains([argv.chainId]); + console.log(`Subscribed to chain ${argv.chainId}. Waiting for opportunities...`); + } + catch (error) { + console.error(error); + this.client.websocket?.close(); + } + } +} +const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) + .option("endpoint-express-relay", { + description: "Express relay endpoint. e.g: https://per-staging.dourolabs.app/", + type: "string", + demandOption: true, +}) + .option("chain-id", { + description: "Chain id to bid on Limo opportunities for. e.g: solana", + type: "string", + demandOption: true, +}) + .option("bid", { + description: "Bid amount in lamports", + type: "string", + default: "100", +}) + .option("private-key", { + description: "Private key of the searcher in base58 format", + type: "string", + conflicts: "private-key-json-file", +}) + .option("private-key-json-file", { + description: "Path to a json file containing the private key of the searcher in array of bytes format", + type: "string", + conflicts: "private-key", +}) + .option("api-key", { + description: "The API key of the searcher to authenticate with the server for fetching and submitting bids", + type: "string", + demandOption: false, +}) + .option("endpoint-svm", { + description: "SVM RPC endpoint", + type: "string", + demandOption: true, +}) + .option("fill-rate", { + description: "How much of the initial order size to fill in percentage. Default is 100%", + type: "number", + default: 100, +}) + .help() + .alias("help", "h") + .parseSync(); +async function run() { + if (!const_1.SVM_CONSTANTS[argv.chainId]) { + throw new Error(`SVM constants not found for chain ${argv.chainId}`); + } + let searcherKeyPair; + if (argv.privateKey) { + const secretKey = anchor.utils.bytes.bs58.decode(argv.privateKey); + searcherKeyPair = web3_js_1.Keypair.fromSecretKey(secretKey); + } + else if (argv.privateKeyJsonFile) { + searcherKeyPair = web3_js_1.Keypair.fromSecretKey(Buffer.from( + // eslint-disable-next-line @typescript-eslint/no-var-requires + JSON.parse(require("fs").readFileSync(argv.privateKeyJsonFile)))); + } + else { + throw new Error("Either private-key or private-key-json-file must be provided"); + } + console.log(`Using searcher pubkey: ${searcherKeyPair.publicKey.toBase58()}`); + const simpleSearcher = new SimpleSearcherLimo(argv.endpointExpressRelay, argv.chainId, searcherKeyPair, argv.endpointSvm, argv.fillRate, argv.apiKey); + await simpleSearcher.start(); +} +run(); diff --git a/express_relay/sdk/js/lib/examples/simpleSearcherSvm.d.ts b/express_relay/sdk/js/lib/examples/simpleSearcherSvm.d.ts new file mode 100644 index 000000000..233599dd8 --- /dev/null +++ b/express_relay/sdk/js/lib/examples/simpleSearcherSvm.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=simpleSearcherSvm.d.ts.map \ No newline at end of file diff --git a/express_relay/sdk/js/lib/examples/simpleSearcherSvm.d.ts.map b/express_relay/sdk/js/lib/examples/simpleSearcherSvm.d.ts.map new file mode 100644 index 000000000..944d4a0cd --- /dev/null +++ b/express_relay/sdk/js/lib/examples/simpleSearcherSvm.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"simpleSearcherSvm.d.ts","sourceRoot":"","sources":["../../src/examples/simpleSearcherSvm.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/express_relay/sdk/js/lib/examples/simpleSearcherSvm.js b/express_relay/sdk/js/lib/examples/simpleSearcherSvm.js new file mode 100644 index 000000000..a33a7aed5 --- /dev/null +++ b/express_relay/sdk/js/lib/examples/simpleSearcherSvm.js @@ -0,0 +1,171 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const yargs_1 = __importDefault(require("yargs")); +const helpers_1 = require("yargs/helpers"); +const index_1 = require("../index"); +const const_1 = require("../const"); +const anchor = __importStar(require("@coral-xyz/anchor")); +const anchor_1 = require("@coral-xyz/anchor"); +const web3_js_1 = require("@solana/web3.js"); +const idlDummy_json_1 = __importDefault(require("./idl/idlDummy.json")); +const svm_1 = require("../svm"); +const DAY_IN_SECONDS = 60 * 60 * 24; +const DUMMY_PIDS = { + "development-solana": new web3_js_1.PublicKey("HYCgALnu6CM2gkQVopa1HGaNf8Vzbs9bomWRiKP267P3"), +}; +class SimpleSearcherSvm { + endpointExpressRelay; + chainId; + privateKey; + endpointSvm; + apiKey; + client; + connectionSvm; + constructor(endpointExpressRelay, chainId, privateKey, endpointSvm, apiKey) { + this.endpointExpressRelay = endpointExpressRelay; + this.chainId = chainId; + this.privateKey = privateKey; + this.endpointSvm = endpointSvm; + this.apiKey = apiKey; + this.client = new index_1.Client({ + baseUrl: endpointExpressRelay, + apiKey, + }, undefined, () => { + return Promise.resolve(); + }, this.bidStatusHandler.bind(this)); + this.connectionSvm = new web3_js_1.Connection(endpointSvm, "confirmed"); + } + async bidStatusHandler(bidStatus) { + let resultDetails = ""; + if (bidStatus.type == "submitted" || bidStatus.type == "won") { + resultDetails = `, transaction ${bidStatus.result}`; + } + else if (bidStatus.type == "lost") { + if (bidStatus.result) { + resultDetails = `, transaction ${bidStatus.result}`; + } + } + console.log(`Bid status for bid ${bidStatus.id}: ${bidStatus.type}${resultDetails}`); + } + async dummyBid() { + const secretKey = anchor.utils.bytes.bs58.decode(this.privateKey); + const searcher = web3_js_1.Keypair.fromSecretKey(secretKey); + const provider = new anchor_1.AnchorProvider(this.connectionSvm, new anchor.Wallet(searcher), {}); + const dummy = new anchor_1.Program(idlDummy_json_1.default, provider); + const permission = web3_js_1.PublicKey.default; + const router = web3_js_1.Keypair.generate().publicKey; + const svmConstants = const_1.SVM_CONSTANTS[this.chainId]; + if (!(this.chainId in DUMMY_PIDS)) { + throw new Error(`Dummy program id not found for chain ${this.chainId}`); + } + const dummyPid = DUMMY_PIDS[this.chainId]; + const configRouter = (0, svm_1.getConfigRouterPda)(this.chainId, router); + const expressRelayMetadata = (0, svm_1.getExpressRelayMetadataPda)(this.chainId); + const accounting = web3_js_1.PublicKey.findProgramAddressSync([anchor.utils.bytes.utf8.encode("accounting")], dummyPid)[0]; + const bidAmount = new anchor.BN(argv.bid); + const ixDummy = await dummy.methods + .doNothing() + .accountsStrict({ + payer: searcher.publicKey, + expressRelay: svmConstants.expressRelayProgram, + expressRelayMetadata, + sysvarInstructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + permission, + router, + configRouter, + accounting, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .instruction(); + ixDummy.programId = dummyPid; + const txRaw = new anchor.web3.Transaction().add(ixDummy); + const expressRelayConfig = await this.client.getExpressRelaySvmConfig(this.chainId, this.connectionSvm); + const bid = await this.client.constructSvmBid(txRaw, searcher.publicKey, router, permission, bidAmount, new anchor.BN(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), this.chainId, expressRelayConfig.relayerSigner, expressRelayConfig.feeReceiverRelayer); + try { + const { blockhash } = await this.connectionSvm.getLatestBlockhash(); + bid.transaction.recentBlockhash = blockhash; + bid.transaction.sign(web3_js_1.Keypair.fromSecretKey(secretKey)); + const bidId = await this.client.submitBid(bid); + console.log(`Successful bid. Bid id ${bidId}`); + } + catch (error) { + console.error(`Failed to bid: ${error}`); + } + } + async start() { + for (;;) { + await this.dummyBid(); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } +} +const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) + .option("endpoint-express-relay", { + description: "Express relay endpoint. e.g: https://per-staging.dourolabs.app/", + type: "string", + demandOption: true, +}) + .option("chain-id", { + description: "Chain id to fetch opportunities for. e.g: solana", + type: "string", + demandOption: true, +}) + .option("bid", { + description: "Bid amount in lamports", + type: "string", + default: "100", +}) + .option("private-key", { + description: "Private key to sign the bid with. In 64-byte base58 format", + type: "string", + demandOption: true, +}) + .option("api-key", { + description: "The API key of the searcher to authenticate with the server for fetching and submitting bids", + type: "string", + demandOption: false, +}) + .option("endpoint-svm", { + description: "SVM RPC endpoint", + type: "string", + demandOption: true, +}) + .help() + .alias("help", "h") + .parseSync(); +async function run() { + if (const_1.SVM_CONSTANTS[argv.chainId] === undefined) { + throw new Error(`SVM constants not found for chain ${argv.chainId}`); + } + const searcherSvm = web3_js_1.Keypair.fromSecretKey(anchor.utils.bytes.bs58.decode(argv.privateKey)); + console.log(`Using searcher pubkey: ${searcherSvm.publicKey.toBase58()}`); + const simpleSearcher = new SimpleSearcherSvm(argv.endpointExpressRelay, argv.chainId, argv.privateKey, argv.endpointSvm, argv.apiKey); + await simpleSearcher.start(); +} +run(); diff --git a/express_relay/sdk/js/lib/idl/idlExpressRelay.json b/express_relay/sdk/js/lib/idl/idlExpressRelay.json new file mode 100644 index 000000000..8cca04d98 --- /dev/null +++ b/express_relay/sdk/js/lib/idl/idlExpressRelay.json @@ -0,0 +1,525 @@ +{ + "address": "GwEtasTAxdS9neVE4GPUpcwR7DB7AizntQSPcG36ubZM", + "metadata": { + "name": "express_relay", + "version": "0.2.0", + "spec": "0.1.0", + "description": "Pyth Express Relay program for handling permissioning and bid distribution", + "repository": "https://github.com/pyth-network/per" + }, + "instructions": [ + { + "name": "check_permission", + "docs": [ + "Checks if permissioning exists for a particular (permission, router) pair within the same transaction", + "Permissioning takes the form of a SubmitBid instruction with matching permission and router accounts", + "Returns the fees paid to the router in the matching instructions" + ], + "discriminator": [154, 199, 232, 242, 96, 72, 197, 236], + "accounts": [ + { + "name": "sysvar_instructions", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "permission" + }, + { + "name": "router" + }, + { + "name": "config_router", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 111, 110, 102, 105, 103, 95, 114, 111, 117, 116, 101, 114 + ] + }, + { + "kind": "account", + "path": "router" + } + ] + } + }, + { + "name": "express_relay_metadata", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + } + ] + } + } + ], + "args": [], + "returns": "u64" + }, + { + "name": "initialize", + "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "express_relay_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + } + ] + } + }, + { + "name": "admin" + }, + { + "name": "relayer_signer" + }, + { + "name": "fee_receiver_relayer" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "data", + "type": { + "defined": { + "name": "InitializeArgs" + } + } + } + ] + }, + { + "name": "set_admin", + "discriminator": [251, 163, 0, 52, 91, 194, 187, 92], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true, + "relations": ["express_relay_metadata"] + }, + { + "name": "express_relay_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + } + ] + } + }, + { + "name": "admin_new" + } + ], + "args": [] + }, + { + "name": "set_relayer", + "discriminator": [23, 243, 33, 88, 110, 84, 196, 37], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true, + "relations": ["express_relay_metadata"] + }, + { + "name": "express_relay_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + } + ] + } + }, + { + "name": "relayer_signer" + }, + { + "name": "fee_receiver_relayer" + } + ], + "args": [] + }, + { + "name": "set_router_split", + "discriminator": [16, 150, 106, 13, 27, 191, 104, 8], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true, + "relations": ["express_relay_metadata"] + }, + { + "name": "config_router", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 111, 110, 102, 105, 103, 95, 114, 111, 117, 116, 101, 114 + ] + }, + { + "kind": "account", + "path": "router" + } + ] + } + }, + { + "name": "express_relay_metadata", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + } + ] + } + }, + { + "name": "router" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "data", + "type": { + "defined": { + "name": "SetRouterSplitArgs" + } + } + } + ] + }, + { + "name": "set_splits", + "discriminator": [175, 2, 86, 49, 225, 202, 232, 189], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true, + "relations": ["express_relay_metadata"] + }, + { + "name": "express_relay_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + } + ] + } + } + ], + "args": [ + { + "name": "data", + "type": { + "defined": { + "name": "SetSplitsArgs" + } + } + } + ] + }, + { + "name": "submit_bid", + "docs": [ + "Submits a bid for a particular (permission, router) pair and distributes bids according to splits" + ], + "discriminator": [19, 164, 237, 254, 64, 139, 237, 93], + "accounts": [ + { + "name": "searcher", + "writable": true, + "signer": true + }, + { + "name": "relayer_signer", + "signer": true, + "relations": ["express_relay_metadata"] + }, + { + "name": "permission" + }, + { + "name": "router", + "writable": true + }, + { + "name": "config_router", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 111, 110, 102, 105, 103, 95, 114, 111, 117, 116, 101, 114 + ] + }, + { + "kind": "account", + "path": "router" + } + ] + } + }, + { + "name": "express_relay_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + } + ] + } + }, + { + "name": "fee_receiver_relayer", + "writable": true, + "relations": ["express_relay_metadata"] + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "sysvar_instructions", + "address": "Sysvar1nstructions1111111111111111111111111" + } + ], + "args": [ + { + "name": "data", + "type": { + "defined": { + "name": "SubmitBidArgs" + } + } + } + ] + }, + { + "name": "withdraw_fees", + "discriminator": [198, 212, 171, 109, 144, 215, 174, 89], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true, + "relations": ["express_relay_metadata"] + }, + { + "name": "fee_receiver_admin", + "writable": true + }, + { + "name": "express_relay_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + } + ] + } + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "ConfigRouter", + "discriminator": [135, 66, 240, 166, 94, 198, 187, 36] + }, + { + "name": "ExpressRelayMetadata", + "discriminator": [204, 75, 133, 7, 175, 241, 130, 11] + } + ], + "errors": [ + { + "code": 6000, + "name": "FeeSplitLargerThanPrecision", + "msg": "Fee split(s) larger than fee precision" + }, + { + "code": 6001, + "name": "FeesHigherThanBid", + "msg": "Fees higher than bid" + }, + { + "code": 6002, + "name": "DeadlinePassed", + "msg": "Deadline passed" + }, + { + "code": 6003, + "name": "InvalidCPISubmitBid", + "msg": "Invalid CPI into submit bid instruction" + }, + { + "code": 6004, + "name": "MissingPermission", + "msg": "Missing permission" + }, + { + "code": 6005, + "name": "MultiplePermissions", + "msg": "Multiple permissions" + }, + { + "code": 6006, + "name": "InsufficientSearcherFunds", + "msg": "Insufficient Searcher Funds" + }, + { + "code": 6007, + "name": "InsufficientRent", + "msg": "Insufficient funds for rent" + } + ], + "types": [ + { + "name": "ConfigRouter", + "type": { + "kind": "struct", + "fields": [ + { + "name": "router", + "type": "pubkey" + }, + { + "name": "split", + "type": "u64" + } + ] + } + }, + { + "name": "ExpressRelayMetadata", + "type": { + "kind": "struct", + "fields": [ + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "relayer_signer", + "type": "pubkey" + }, + { + "name": "fee_receiver_relayer", + "type": "pubkey" + }, + { + "name": "split_router_default", + "type": "u64" + }, + { + "name": "split_relayer", + "type": "u64" + } + ] + } + }, + { + "name": "InitializeArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "split_router_default", + "type": "u64" + }, + { + "name": "split_relayer", + "type": "u64" + } + ] + } + }, + { + "name": "SetRouterSplitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "split_router", + "type": "u64" + } + ] + } + }, + { + "name": "SetSplitsArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "split_router_default", + "type": "u64" + }, + { + "name": "split_relayer", + "type": "u64" + } + ] + } + }, + { + "name": "SubmitBidArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "deadline", + "type": "i64" + }, + { + "name": "bid_amount", + "type": "u64" + } + ] + } + } + ] +} diff --git a/express_relay/sdk/js/lib/index.d.ts b/express_relay/sdk/js/lib/index.d.ts new file mode 100644 index 000000000..5b7b11904 --- /dev/null +++ b/express_relay/sdk/js/lib/index.d.ts @@ -0,0 +1,145 @@ +import type { components } from "./serverTypes"; +import { ClientOptions as FetchClientOptions } from "openapi-fetch"; +import { Address, Hex } from "viem"; +import WebSocket from "isomorphic-ws"; +import { Bid, BidId, BidParams, BidsResponse, BidStatusUpdate, BidSvm, ExpressRelaySvmConfig, Opportunity, OpportunityBid, OpportunityEvm, OpportunityCreate, TokenAmount } from "./types"; +import { Connection, PublicKey, Transaction, TransactionInstruction } from "@solana/web3.js"; +import * as anchor from "@coral-xyz/anchor"; +export * from "./types"; +export * from "./const"; +export declare class ClientError extends Error { +} +type ClientOptions = FetchClientOptions & { + baseUrl: string; + apiKey?: string; +}; +export interface WsOptions { + /** + * Max time to wait for a response from the server in milliseconds + */ + response_timeout: number; +} +export declare function checkHex(hex: string): Hex; +export declare function checkAddress(address: string): Address; +export declare function checkTokenQty(token: { + token: string; + amount: string; +}): TokenAmount; +export declare class Client { + clientOptions: ClientOptions; + wsOptions: WsOptions; + websocket?: WebSocket; + idCounter: number; + callbackRouter: Record void>; + private websocketOpportunityCallback?; + private websocketBidStatusCallback?; + private getAuthorization; + constructor(clientOptions: ClientOptions, wsOptions?: WsOptions, opportunityCallback?: (opportunity: Opportunity) => Promise, bidStatusCallback?: (statusUpdate: BidStatusUpdate) => Promise); + private connectWebsocket; + /** + * Subscribes to the specified chains + * + * The opportunity handler will be called for opportunities on the specified chains + * If the opportunity handler is not set, an error will be thrown + * @param chains + */ + subscribeChains(chains: string[]): Promise; + /** + * Unsubscribes from the specified chains + * + * The opportunity handler will no longer be called for opportunities on the specified chains + * @param chains + */ + unsubscribeChains(chains: string[]): Promise; + requestViaWebsocket(msg: components["schemas"]["ClientMessage"]): Promise; + /** + * Fetches opportunities + * @param chainId Chain id to fetch opportunities for. e.g: sepolia + * @returns List of opportunities + */ + getOpportunities(chainId?: string): Promise; + /** + * Submits an opportunity to be exposed to searchers + * @param opportunity Opportunity to submit + */ + submitOpportunity(opportunity: OpportunityCreate): Promise; + /** + * Submits a raw bid for a permission key + * @param bid + * @param subscribeToUpdates If true, the client will subscribe to bid status updates via websocket and will call the bid status callback if set + * @returns The id of the submitted bid, you can use this id to track the status of the bid + */ + submitBid(bid: Bid, subscribeToUpdates?: boolean): Promise; + /** + * Get bids for an api key + * @param fromTime The datetime to fetch bids from. If undefined or null, fetches from the beginning of time. + * @returns The paginated bids response + */ + getBids(fromTime?: Date): Promise; + private toServerBid; + /** + * 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 + */ + convertOpportunity(opportunity: components["schemas"]["Opportunity"]): Opportunity | undefined; + /** + * Creates a signed opportunity bid for an opportunity + * @param opportunity EVM 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 + */ + signOpportunityBid(opportunity: OpportunityEvm, bidParams: BidParams, privateKey: Hex): Promise; + /** + * Creates a signed bid for an EVM opportunity + * @param opportunity EVM Opportunity to bid on + * @param bidParams Bid amount, nonce, and deadline timestamp + * @param privateKey Private key to sign the bid with + * @returns Signed bid + */ + signBid(opportunity: OpportunityEvm, bidParams: BidParams, privateKey: Hex): Promise; + /** + * Creates a signature for the bid and opportunity + * @param opportunity EVM Opportunity to bid on + * @param bidParams Bid amount, nonce, and deadline timestamp + * @param privateKey Private key to sign the bid with + * @returns Signature for the bid and opportunity + */ + getSignature(opportunity: OpportunityEvm, bidParams: BidParams, privateKey: Hex): Promise<`0x${string}`>; + /** + * Fetches the Express Relay SVM config necessary for bidding + * @param chainId The id for the chain you want to fetch the config for + * @param connection The connection to use for fetching the config + */ + getExpressRelaySvmConfig(chainId: string, connection: Connection): Promise; + /** + * Constructs a SubmitBid instruction, which can be added to a transaction to permission it on the given permission key + * @param searcher The address of the searcher that is submitting the bid + * @param router The identifying address of the router that the permission key is for + * @param permissionKey The 32-byte permission key as an SVM PublicKey + * @param bidAmount The amount of the bid in lamports + * @param deadline The deadline for the bid in seconds since Unix epoch + * @param chainId The chain ID as a string, e.g. "solana" + * @param relayerSigner The address of the relayer that is submitting the bid + * @param feeReceiverRelayer The fee collection address of the relayer + * @returns The SubmitBid instruction + */ + constructSubmitBidInstruction(searcher: PublicKey, router: PublicKey, permissionKey: PublicKey, bidAmount: anchor.BN, deadline: anchor.BN, chainId: string, relayerSigner: PublicKey, feeReceiverRelayer: PublicKey): Promise; + /** + * Constructs an SVM bid, by adding a SubmitBid instruction to a transaction + * @param tx The transaction to add a SubmitBid instruction to. This transaction should already check for the appropriate permissions. + * @param searcher The address of the searcher that is submitting the bid + * @param router The identifying address of the router that the permission key is for + * @param permissionKey The 32-byte permission key as an SVM PublicKey + * @param bidAmount The amount of the bid in lamports + * @param deadline The deadline for the bid in seconds since Unix epoch + * @param chainId The chain ID as a string, e.g. "solana" + * @param relayerSigner The address of the relayer that is submitting the bid + * @param feeReceiverRelayer The fee collection address of the relayer + * @returns The constructed SVM bid + */ + constructSvmBid(tx: Transaction, searcher: PublicKey, router: PublicKey, permissionKey: PublicKey, bidAmount: anchor.BN, deadline: anchor.BN, chainId: string, relayerSigner: PublicKey, feeReceiverRelayer: PublicKey): Promise; +} +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/express_relay/sdk/js/lib/index.d.ts.map b/express_relay/sdk/js/lib/index.d.ts.map new file mode 100644 index 000000000..6cfeba048 --- /dev/null +++ b/express_relay/sdk/js/lib/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/express_relay/sdk/js/lib/index.js b/express_relay/sdk/js/lib/index.js new file mode 100644 index 000000000..64f533d82 --- /dev/null +++ b/express_relay/sdk/js/lib/index.js @@ -0,0 +1,465 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Client = exports.ClientError = void 0; +exports.checkHex = checkHex; +exports.checkAddress = checkAddress; +exports.checkTokenQty = checkTokenQty; +const openapi_fetch_1 = __importDefault(require("openapi-fetch")); +const viem_1 = require("viem"); +const isomorphic_ws_1 = __importDefault(require("isomorphic-ws")); +const web3_js_1 = require("@solana/web3.js"); +const limo_sdk_1 = require("@kamino-finance/limo-sdk"); +const utils_1 = require("@kamino-finance/limo-sdk/dist/utils"); +const evm = __importStar(require("./evm")); +const svm = __importStar(require("./svm")); +__exportStar(require("./types"), exports); +__exportStar(require("./const"), exports); +class ClientError extends Error { +} +exports.ClientError = ClientError; +const DEFAULT_WS_OPTIONS = { + response_timeout: 10000, +}; +function checkHex(hex) { + if ((0, viem_1.isHex)(hex)) { + return hex; + } + throw new ClientError(`Invalid hex: ${hex}`); +} +function checkAddress(address) { + if ((0, viem_1.isAddress)(address)) { + return address; + } + throw new ClientError(`Invalid address: ${address}`); +} +function checkTokenQty(token) { + return { + token: checkAddress(token.token), + amount: BigInt(token.amount), + }; +} +class Client { + clientOptions; + wsOptions; + websocket; + idCounter = 0; + callbackRouter = {}; + websocketOpportunityCallback; + websocketBidStatusCallback; + getAuthorization() { + return this.clientOptions.apiKey + ? { + Authorization: `Bearer ${this.clientOptions.apiKey}`, + } + : {}; + } + constructor(clientOptions, wsOptions, opportunityCallback, bidStatusCallback) { + this.clientOptions = clientOptions; + this.clientOptions.headers = { + ...(this.clientOptions.headers ?? {}), + ...this.getAuthorization(), + }; + this.wsOptions = { ...DEFAULT_WS_OPTIONS, ...wsOptions }; + this.websocketOpportunityCallback = opportunityCallback; + this.websocketBidStatusCallback = bidStatusCallback; + } + connectWebsocket() { + const websocketEndpoint = new URL(this.clientOptions.baseUrl); + websocketEndpoint.protocol = + websocketEndpoint.protocol === "https:" ? "wss:" : "ws:"; + websocketEndpoint.pathname = "/v1/ws"; + this.websocket = new isomorphic_ws_1.default(websocketEndpoint.toString(), { + headers: this.getAuthorization(), + }); + this.websocket.on("message", async (data) => { + const message = JSON.parse(data.toString()); + if ("type" in message && message.type === "new_opportunity") { + if (this.websocketOpportunityCallback !== undefined) { + const convertedOpportunity = this.convertOpportunity(message.opportunity); + if (convertedOpportunity !== undefined) { + await this.websocketOpportunityCallback(convertedOpportunity); + } + } + } + else if ("type" in message && message.type === "bid_status_update") { + if (this.websocketBidStatusCallback !== undefined) { + await this.websocketBidStatusCallback({ + id: message.status.id, + ...message.status.bid_status, + }); + } + } + else if ("id" in message && message.id) { + // Response to a request sent earlier via the websocket with the same id + const callback = this.callbackRouter[message.id]; + if (callback !== undefined) { + callback(message); + delete this.callbackRouter[message.id]; + } + } + else if ("error" in message) { + // Can not route error messages to the callback router as they don't have an id + console.error(message.error); + } + }); + } + /** + * Subscribes to the specified chains + * + * The opportunity handler will be called for opportunities on the specified chains + * If the opportunity handler is not set, an error will be thrown + * @param chains + */ + async subscribeChains(chains) { + if (this.websocketOpportunityCallback === undefined) { + throw new ClientError("Opportunity handler not set"); + } + await this.requestViaWebsocket({ + method: "subscribe", + params: { + chain_ids: chains, + }, + }); + } + /** + * Unsubscribes from the specified chains + * + * The opportunity handler will no longer be called for opportunities on the specified chains + * @param chains + */ + async unsubscribeChains(chains) { + await this.requestViaWebsocket({ + method: "unsubscribe", + params: { + chain_ids: chains, + }, + }); + } + async requestViaWebsocket(msg) { + const msg_with_id = { + ...msg, + id: (this.idCounter++).toString(), + }; + return new Promise((resolve, reject) => { + this.callbackRouter[msg_with_id.id] = (response) => { + if (response.status === "success") { + resolve(response.result); + } + else { + reject(response.result); + } + }; + if (this.websocket === undefined) { + this.connectWebsocket(); + } + if (this.websocket !== undefined) { + if (this.websocket.readyState === isomorphic_ws_1.default.CONNECTING) { + this.websocket.on("open", () => { + this.websocket?.send(JSON.stringify(msg_with_id)); + }); + } + else if (this.websocket.readyState === isomorphic_ws_1.default.OPEN) { + this.websocket.send(JSON.stringify(msg_with_id)); + } + else { + reject("Websocket connection closing or already closed"); + } + } + setTimeout(() => { + delete this.callbackRouter[msg_with_id.id]; + reject("Websocket response timeout"); + }, this.wsOptions.response_timeout); + }); + } + /** + * Fetches opportunities + * @param chainId Chain id to fetch opportunities for. e.g: sepolia + * @returns List of opportunities + */ + async getOpportunities(chainId) { + const client = (0, openapi_fetch_1.default)(this.clientOptions); + const opportunities = await client.GET("/v1/opportunities", { + params: { query: { chain_id: chainId } }, + }); + if (opportunities.data === undefined) { + throw new ClientError("No opportunities found"); + } + return opportunities.data.flatMap((opportunity) => { + const convertedOpportunity = this.convertOpportunity(opportunity); + if (convertedOpportunity === undefined) { + return []; + } + return convertedOpportunity; + }); + } + /** + * Submits an opportunity to be exposed to searchers + * @param opportunity Opportunity to submit + */ + async submitOpportunity(opportunity) { + const client = (0, openapi_fetch_1.default)(this.clientOptions); + let body; + if ("order" in opportunity) { + const encoded_order = Buffer.alloc(limo_sdk_1.Order.discriminator.length + limo_sdk_1.Order.layout.span); + limo_sdk_1.Order.discriminator.copy(encoded_order); + limo_sdk_1.Order.layout.encode(opportunity.order.state, encoded_order, limo_sdk_1.Order.discriminator.length); + body = { + chain_id: opportunity.chainId, + version: "v1", + program: opportunity.program, + order: encoded_order.toString("base64"), + slot: opportunity.slot, + block_hash: opportunity.blockHash, + order_address: opportunity.order.address.toBase58(), + buy_tokens: [ + { + token: opportunity.order.state.inputMint.toBase58(), + amount: opportunity.order.state.remainingInputAmount.toNumber(), + }, + ], + sell_tokens: [ + { + token: opportunity.order.state.outputMint.toBase58(), + amount: opportunity.order.state.expectedOutputAmount.toNumber(), + }, + ], + permission_account: opportunity.order.address.toBase58(), + router: (0, utils_1.getPdaAuthority)(limo_sdk_1.limoId, opportunity.order.state.globalConfig).toBase58(), + }; + } + else { + body = { + chain_id: opportunity.chainId, + version: "v1", + permission_key: opportunity.permissionKey, + target_contract: opportunity.targetContract, + target_calldata: opportunity.targetCalldata, + target_call_value: opportunity.targetCallValue.toString(), + sell_tokens: opportunity.sellTokens.map(({ token, amount }) => ({ + token, + amount: amount.toString(), + })), + buy_tokens: opportunity.buyTokens.map(({ token, amount }) => ({ + token, + amount: amount.toString(), + })), + }; + } + const response = await client.POST("/v1/opportunities", { + body: body, + }); + if (response.error) { + throw new ClientError(response.error.error); + } + } + /** + * Submits a raw bid for a permission key + * @param bid + * @param subscribeToUpdates If true, the client will subscribe to bid status updates via websocket and will call the bid status callback if set + * @returns The id of the submitted bid, you can use this id to track the status of the bid + */ + async submitBid(bid, subscribeToUpdates = true) { + const serverBid = this.toServerBid(bid); + if (subscribeToUpdates) { + const result = await this.requestViaWebsocket({ + method: "post_bid", + params: { + bid: serverBid, + }, + }); + if (result === null) { + throw new ClientError("Empty response in websocket for bid submission"); + } + return result.id; + } + else { + const client = (0, openapi_fetch_1.default)(this.clientOptions); + const response = await client.POST("/v1/bids", { + body: serverBid, + }); + if (response.error) { + throw new ClientError(response.error.error); + } + else if (response.data === undefined) { + throw new ClientError("No data returned"); + } + else { + return response.data.id; + } + } + } + /** + * Get bids for an api key + * @param fromTime The datetime to fetch bids from. If undefined or null, fetches from the beginning of time. + * @returns The paginated bids response + */ + async getBids(fromTime) { + const client = (0, openapi_fetch_1.default)(this.clientOptions); + const response = await client.GET("/v1/bids", { + params: { query: { from_time: fromTime?.toISOString() } }, + }); + if (response.error) { + throw new ClientError(response.error.error); + } + else if (response.data === undefined) { + throw new ClientError("No data returned"); + } + else { + return response.data; + } + } + toServerBid(bid) { + if (bid.env === "evm") { + return { + amount: bid.amount.toString(), + target_calldata: bid.targetCalldata, + chain_id: bid.chainId, + target_contract: bid.targetContract, + permission_key: bid.permissionKey, + }; + } + return { + chain_id: bid.chainId, + transaction: bid.transaction + .serialize({ requireAllSignatures: false }) + .toString("base64"), + }; + } + /** + * 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 + */ + convertOpportunity(opportunity) { + if (opportunity.version !== "v1") { + console.warn(`Can not handle opportunity version: ${opportunity.version}. Please upgrade your client.`); + return undefined; + } + if ("target_calldata" in opportunity) { + return { + chainId: opportunity.chain_id, + opportunityId: opportunity.opportunity_id, + permissionKey: checkHex(opportunity.permission_key), + targetContract: checkAddress(opportunity.target_contract), + targetCalldata: checkHex(opportunity.target_calldata), + targetCallValue: BigInt(opportunity.target_call_value), + sellTokens: opportunity.sell_tokens.map(checkTokenQty), + buyTokens: opportunity.buy_tokens.map(checkTokenQty), + }; + } + const order = limo_sdk_1.Order.decode(Buffer.from(opportunity.order, "base64")); + return { + chainId: opportunity.chain_id, + slot: opportunity.slot, + blockHash: opportunity.block_hash, + opportunityId: opportunity.opportunity_id, + order: { + state: order, + address: new web3_js_1.PublicKey(opportunity.order_address), + }, + program: "limo", + }; + } + // EVM specific functions + /** + * Creates a signed opportunity bid for an opportunity + * @param opportunity EVM 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, bidParams, privateKey) { + return evm.signOpportunityBid(opportunity, bidParams, privateKey); + } + /** + * Creates a signed bid for an EVM opportunity + * @param opportunity EVM Opportunity to bid on + * @param bidParams Bid amount, nonce, and deadline timestamp + * @param privateKey Private key to sign the bid with + * @returns Signed bid + */ + async signBid(opportunity, bidParams, privateKey) { + return evm.signBid(opportunity, bidParams, privateKey); + } + /** + * Creates a signature for the bid and opportunity + * @param opportunity EVM Opportunity to bid on + * @param bidParams Bid amount, nonce, and deadline timestamp + * @param privateKey Private key to sign the bid with + * @returns Signature for the bid and opportunity + */ + async getSignature(opportunity, bidParams, privateKey) { + return evm.getSignature(opportunity, bidParams, privateKey); + } + // SVM specific functions + /** + * Fetches the Express Relay SVM config necessary for bidding + * @param chainId The id for the chain you want to fetch the config for + * @param connection The connection to use for fetching the config + */ + async getExpressRelaySvmConfig(chainId, connection) { + return svm.getExpressRelaySvmConfig(chainId, connection); + } + /** + * Constructs a SubmitBid instruction, which can be added to a transaction to permission it on the given permission key + * @param searcher The address of the searcher that is submitting the bid + * @param router The identifying address of the router that the permission key is for + * @param permissionKey The 32-byte permission key as an SVM PublicKey + * @param bidAmount The amount of the bid in lamports + * @param deadline The deadline for the bid in seconds since Unix epoch + * @param chainId The chain ID as a string, e.g. "solana" + * @param relayerSigner The address of the relayer that is submitting the bid + * @param feeReceiverRelayer The fee collection address of the relayer + * @returns The SubmitBid instruction + */ + async constructSubmitBidInstruction(searcher, router, permissionKey, bidAmount, deadline, chainId, relayerSigner, feeReceiverRelayer) { + return svm.constructSubmitBidInstruction(searcher, router, permissionKey, bidAmount, deadline, chainId, relayerSigner, feeReceiverRelayer); + } + /** + * Constructs an SVM bid, by adding a SubmitBid instruction to a transaction + * @param tx The transaction to add a SubmitBid instruction to. This transaction should already check for the appropriate permissions. + * @param searcher The address of the searcher that is submitting the bid + * @param router The identifying address of the router that the permission key is for + * @param permissionKey The 32-byte permission key as an SVM PublicKey + * @param bidAmount The amount of the bid in lamports + * @param deadline The deadline for the bid in seconds since Unix epoch + * @param chainId The chain ID as a string, e.g. "solana" + * @param relayerSigner The address of the relayer that is submitting the bid + * @param feeReceiverRelayer The fee collection address of the relayer + * @returns The constructed SVM bid + */ + async constructSvmBid(tx, searcher, router, permissionKey, bidAmount, deadline, chainId, relayerSigner, feeReceiverRelayer) { + return svm.constructSvmBid(tx, searcher, router, permissionKey, bidAmount, deadline, chainId, relayerSigner, feeReceiverRelayer); + } +} +exports.Client = Client; diff --git a/express_relay/sdk/js/lib/svm.d.ts b/express_relay/sdk/js/lib/svm.d.ts new file mode 100644 index 000000000..fa123ec13 --- /dev/null +++ b/express_relay/sdk/js/lib/svm.d.ts @@ -0,0 +1,9 @@ +import { Connection, PublicKey, Transaction, TransactionInstruction } from "@solana/web3.js"; +import * as anchor from "@coral-xyz/anchor"; +import { BidSvm, ExpressRelaySvmConfig } from "./types"; +export declare function getConfigRouterPda(chain: string, router: PublicKey): PublicKey; +export declare function getExpressRelayMetadataPda(chain: string): PublicKey; +export declare function constructSubmitBidInstruction(searcher: PublicKey, router: PublicKey, permissionKey: PublicKey, bidAmount: anchor.BN, deadline: anchor.BN, chainId: string, relayerSigner: PublicKey, feeReceiverRelayer: PublicKey): Promise; +export declare function constructSvmBid(tx: Transaction, searcher: PublicKey, router: PublicKey, permissionKey: PublicKey, bidAmount: anchor.BN, deadline: anchor.BN, chainId: string, relayerSigner: PublicKey, feeReceiverRelayer: PublicKey): Promise; +export declare function getExpressRelaySvmConfig(chainId: string, connection: Connection): Promise; +//# sourceMappingURL=svm.d.ts.map \ No newline at end of file diff --git a/express_relay/sdk/js/lib/svm.d.ts.map b/express_relay/sdk/js/lib/svm.d.ts.map new file mode 100644 index 000000000..ff9aa3c13 --- /dev/null +++ b/express_relay/sdk/js/lib/svm.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"svm.d.ts","sourceRoot":"","sources":["../src/svm.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EAEV,SAAS,EACT,WAAW,EACX,sBAAsB,EACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAcxD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,SAAS,GAChB,SAAS,CAOX;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAOnE;AAED,wBAAsB,6BAA6B,CACjD,QAAQ,EAAE,SAAS,EACnB,MAAM,EAAE,SAAS,EACjB,aAAa,EAAE,SAAS,EACxB,SAAS,EAAE,MAAM,CAAC,EAAE,EACpB,QAAQ,EAAE,MAAM,CAAC,EAAE,EACnB,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,SAAS,EACxB,kBAAkB,EAAE,SAAS,GAC5B,OAAO,CAAC,sBAAsB,CAAC,CA8BjC;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,SAAS,EACnB,MAAM,EAAE,SAAS,EACjB,aAAa,EAAE,SAAS,EACxB,SAAS,EAAE,MAAM,CAAC,EAAE,EACpB,QAAQ,EAAE,MAAM,CAAC,EAAE,EACnB,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,SAAS,EACxB,kBAAkB,EAAE,SAAS,GAC5B,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,qBAAqB,CAAC,CAgBhC"} \ No newline at end of file diff --git a/express_relay/sdk/js/lib/svm.js b/express_relay/sdk/js/lib/svm.js new file mode 100644 index 000000000..7e5e231f5 --- /dev/null +++ b/express_relay/sdk/js/lib/svm.js @@ -0,0 +1,96 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getConfigRouterPda = getConfigRouterPda; +exports.getExpressRelayMetadataPda = getExpressRelayMetadataPda; +exports.constructSubmitBidInstruction = constructSubmitBidInstruction; +exports.constructSvmBid = constructSvmBid; +exports.getExpressRelaySvmConfig = getExpressRelaySvmConfig; +const web3_js_1 = require("@solana/web3.js"); +const anchor = __importStar(require("@coral-xyz/anchor")); +const anchor_1 = require("@coral-xyz/anchor"); +const idlExpressRelay_json_1 = __importDefault(require("./idl/idlExpressRelay.json")); +const const_1 = require("./const"); +const nodewallet_1 = __importDefault(require("@coral-xyz/anchor/dist/cjs/nodewallet")); +function getExpressRelayProgram(chain) { + if (!const_1.SVM_CONSTANTS[chain]) { + throw new Error(`Chain ${chain} not supported`); + } + return const_1.SVM_CONSTANTS[chain].expressRelayProgram; +} +function getConfigRouterPda(chain, router) { + const expressRelayProgram = getExpressRelayProgram(chain); + return web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("config_router"), router.toBuffer()], expressRelayProgram)[0]; +} +function getExpressRelayMetadataPda(chain) { + const expressRelayProgram = getExpressRelayProgram(chain); + return web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("metadata")], expressRelayProgram)[0]; +} +async function constructSubmitBidInstruction(searcher, router, permissionKey, bidAmount, deadline, chainId, relayerSigner, feeReceiverRelayer) { + const expressRelay = new anchor_1.Program(idlExpressRelay_json_1.default, {}); + const configRouter = getConfigRouterPda(chainId, router); + const expressRelayMetadata = getExpressRelayMetadataPda(chainId); + const svmConstants = const_1.SVM_CONSTANTS[chainId]; + const ixSubmitBid = await expressRelay.methods + .submitBid({ + deadline, + bidAmount, + }) + .accountsStrict({ + searcher, + relayerSigner, + permission: permissionKey, + router, + configRouter, + expressRelayMetadata, + feeReceiverRelayer, + systemProgram: anchor.web3.SystemProgram.programId, + sysvarInstructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction(); + ixSubmitBid.programId = svmConstants.expressRelayProgram; + return ixSubmitBid; +} +async function constructSvmBid(tx, searcher, router, permissionKey, bidAmount, deadline, chainId, relayerSigner, feeReceiverRelayer) { + const ixSubmitBid = await constructSubmitBidInstruction(searcher, router, permissionKey, bidAmount, deadline, chainId, relayerSigner, feeReceiverRelayer); + tx.instructions.unshift(ixSubmitBid); + return { + transaction: tx, + chainId: chainId, + env: "svm", + }; +} +async function getExpressRelaySvmConfig(chainId, connection) { + const provider = new anchor_1.AnchorProvider(connection, new nodewallet_1.default(new web3_js_1.Keypair())); + const expressRelay = new anchor_1.Program(idlExpressRelay_json_1.default, provider); + const metadata = await expressRelay.account.expressRelayMetadata.fetch(getExpressRelayMetadataPda(chainId)); + return { + feeReceiverRelayer: metadata.feeReceiverRelayer, + relayerSigner: metadata.relayerSigner, + }; +} diff --git a/express_relay/sdk/js/lib/types.d.ts b/express_relay/sdk/js/lib/types.d.ts new file mode 100644 index 000000000..6cea4c48a --- /dev/null +++ b/express_relay/sdk/js/lib/types.d.ts @@ -0,0 +1,231 @@ +import { Address, Hex } from "viem"; +import type { components } from "./serverTypes"; +import { Blockhash, PublicKey, Transaction } from "@solana/web3.js"; +import { OrderStateAndAddress } from "@kamino-finance/limo-sdk/dist/utils"; +/** + * ERC20 token with contract address and amount + */ +export type TokenAmount = { + token: Address; + amount: bigint; +}; +/** + * TokenPermissions struct for permit2 + */ +export type TokenPermissions = { + token: Address; + amount: bigint; +}; +export type BidId = string; +export type ChainId = string; +/** + * Bid parameters + */ +export type BidParams = { + /** + * Bid amount in wei + */ + amount: bigint; + /** + * Bid nonce, used to prevent replay of a submitted signature. + * This can be set to a random uint256 when creating a new signature + */ + nonce: bigint; + /** + * Unix timestamp for when the bid is no longer valid in seconds + */ + deadline: bigint; +}; +export type OpportunityAdapterConfig = { + /** + * The chain id as a u64 + */ + chain_id: number; + /** + * The opportunity factory address + */ + opportunity_adapter_factory: Address; + /** + * The hash of the bytecode used to initialize the opportunity adapter + */ + opportunity_adapter_init_bytecode_hash: Hex; + /** + * The permit2 address + */ + permit2: Address; + /** + * The weth address + */ + weth: Address; +}; +/** + * Represents a valid opportunity ready to be executed + */ +export type OpportunityEvm = { + /** + * The chain id where the opportunity will be executed. + */ + chainId: ChainId; + /** + * Permission key required for successful execution of the opportunity. + */ + permissionKey: Hex; + /** + * Contract address to call for execution of the opportunity. + */ + targetContract: Address; + /** + * Calldata for the targetContract call. + */ + targetCalldata: Hex; + /** + * Value to send with the targetContract call. + */ + targetCallValue: bigint; + /** + * Tokens required to execute the opportunity + */ + sellTokens: TokenAmount[]; + /** + * Tokens to receive after the opportunity is executed + */ + buyTokens: TokenAmount[]; + /** + * Unique identifier for the opportunity + */ + opportunityId: string; +}; +export type OpportunitySvm = { + order: OrderStateAndAddress; + program: "limo"; + /** + * The chain id where the opportunity will be executed. + */ + chainId: ChainId; + /** + * Slot where the opportunity was found + */ + slot: number; + /** + * Blockhash that can be used to sign transactions for this opportunity + */ + blockHash: Blockhash; + /** + * Unique identifier for the opportunity + */ + opportunityId: string; +}; +export type OpportunityCreate = Omit | Omit; +export type Opportunity = OpportunityEvm | OpportunitySvm; +/** + * Represents a bid for an opportunity + */ +export type OpportunityBid = { + /** + * Opportunity unique identifier in uuid format + */ + opportunityId: string; + /** + * The permission key required for successful execution of the opportunity. + */ + permissionKey: Hex; + /** + * Executor address + */ + executor: Address; + /** + * Signature of the executor + */ + signature: Hex; + bid: BidParams; +}; +/** + * All the parameters necessary to represent an opportunity + */ +export type Bid = BidEvm | BidSvm; +/** + * Represents a raw EVM bid on acquiring a permission key + */ +export type BidEvm = { + /** + * The permission key to bid on + * @example 0xc0ffeebabe + * + */ + permissionKey: Hex; + /** + * @description Amount of bid in wei. + * @example 10 + */ + amount: bigint; + /** + * @description Calldata for the targetContract call. + * @example 0xdeadbeef + */ + targetCalldata: Hex; + /** + * @description The chain id to bid on. + * @example sepolia + */ + chainId: ChainId; + /** + * @description The targetContract address to call. + * @example 0xcA11bde05977b3631167028862bE2a173976CA11 + */ + targetContract: Address; + /** + * @description The execution environment for the bid. + */ + env: "evm"; +}; +/** + * Necessary accounts for submitting a SVM bid. These can be fetched from on-chain program data. + */ +export type ExpressRelaySvmConfig = { + /** + * @description The relayer signer account. All submitted transactions will be signed by this account. + */ + relayerSigner: PublicKey; + /** + * @description The fee collection account for the relayer. + */ + feeReceiverRelayer: PublicKey; +}; +/** + * Represents a raw SVM bid on acquiring a permission key + */ +export type BidSvm = { + /** + * @description Transaction object. + * @example SGVsbG8sIFdvcmxkIQ + */ + transaction: Transaction; + /** + * @description The chain id to bid on. + * @example solana + */ + chainId: ChainId; + /** + * @description The execution environment for the bid. + */ + env: "svm"; +}; +export type BidStatusUpdate = { + id: BidId; +} & components["schemas"]["BidStatus"]; +export type BidStatusUpdateSvm = { + id: BidId; +} & components["schemas"]["BidStatusSvm"]; +export type BidStatusUpdateEvm = { + id: BidId; +} & components["schemas"]["BidStatusEvm"]; +export type BidResponse = components["schemas"]["SimulatedBid"]; +export type BidResponseSvm = components["schemas"]["SimulatedBidSvm"]; +export type BidResponseEvm = components["schemas"]["SimulatedBidEvm"]; +export type BidsResponse = { + items: BidResponse[]; +}; +export type SvmConstantsConfig = { + expressRelayProgram: PublicKey; +}; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/express_relay/sdk/js/lib/types.d.ts.map b/express_relay/sdk/js/lib/types.d.ts.map new file mode 100644 index 000000000..34fef0439 --- /dev/null +++ b/express_relay/sdk/js/lib/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/express_relay/sdk/js/lib/types.js b/express_relay/sdk/js/lib/types.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/express_relay/sdk/js/lib/types.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a40cf485..0b751056c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -447,6 +447,9 @@ importers: '@zodios/core': specifier: ^10.9.6 version: 10.9.6(axios@1.7.7)(zod@3.23.8) + bn.js: + specifier: ^5.2.1 + version: 5.2.1 eventsource: specifier: ^2.0.2 version: 2.0.2 @@ -454,6 +457,9 @@ importers: specifier: ^3.23.8 version: 3.23.8 devDependencies: + '@types/bn.js': + specifier: ^5.1.5 + version: 5.1.6 '@types/eventsource': specifier: ^1.1.15 version: 1.1.15