-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
515 additions
and
10 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
// This is a sample script that will submit an order to the Lyra testnet | ||
|
||
// To run this script, you must have a Lyra testnet account with some ETH and LYRA in it | ||
|
||
|
||
const { ethers } = require("ethers"); | ||
const { WebSocket } = require('ws'); | ||
const dotenv = require('dotenv'); | ||
|
||
dotenv.config(); | ||
|
||
const PRIVATE_KEY = process.env.OWNER_PRIVATE_KEY | ||
const PROVIDER_URL = 'https://l2-prod-testnet-0eakp60405.t.conduit.xyz'; | ||
const WS_ADDRESS = process.env.WEBSOCKET_ADDRESS || 'ws://localhost:3000/ws'; | ||
const ACTION_TYPEHASH = '0x4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17'; | ||
const DOMAIN_SEPARATOR = '0xff2ba7c8d1c63329d3c2c6c9c19113440c004c51fe6413f65654962afaff00f3'; | ||
// const ASSET_ADDRESS = '0x8932cc48F7AD0c6c7974606cFD7bCeE2F543a124'; | ||
const ASSET_ADDRESS = '0x62CF2Cc6450Dc3FbD0662Bfd69af0a4D7485Fe4E'; | ||
const TRADE_MODULE_ADDRESS = '0x63Bc9D10f088eddc39A6c40Ff81E99516dfD5269'; | ||
|
||
const PROVIDER = new ethers.JsonRpcProvider(PROVIDER_URL); | ||
const wallet = new ethers.Wallet(PRIVATE_KEY, PROVIDER); | ||
const encoder = ethers.AbiCoder.defaultAbiCoder(); | ||
const subaccount_id = 550 | ||
|
||
const OPTION_NAME = 'ETH-PERP' | ||
const OPTION_SUB_ID = '0' // can retreive with public/get_instrument | ||
|
||
|
||
async function signAuthenticationHeader() { | ||
const timestamp = Date.now().toString(); | ||
const signature = await wallet.signMessage(timestamp); | ||
return { | ||
wallet: wallet.address, | ||
timestamp: timestamp, | ||
signature: signature, | ||
}; | ||
} | ||
|
||
const connectWs = async () => { | ||
return new Promise((resolve, reject) => { | ||
const ws = new WebSocket(WS_ADDRESS); | ||
|
||
ws.on('open', () => { | ||
setTimeout(() => resolve(ws), 50); | ||
}); | ||
|
||
ws.on('error', reject); | ||
|
||
ws.on('close', (code, reason) => { | ||
if (code && reason.toString()) { | ||
console.log(`WebSocket closed with code: ${code}`, `Reason: ${reason}`); | ||
} | ||
}); | ||
}); | ||
}; | ||
|
||
async function loginClient(wsc ) { | ||
const login_request = JSON.stringify({ | ||
method: 'public/login', | ||
params: await signAuthenticationHeader(), | ||
id: Math.floor(Math.random() * 10000) | ||
}); | ||
wsc.send(login_request); | ||
console.log('Sent login request') | ||
console.log(login_request) | ||
await new Promise(resolve => setTimeout(resolve, 2000)); | ||
} | ||
|
||
function defineOrder(){ | ||
return { | ||
instrument_name: OPTION_NAME, | ||
subaccount_id: subaccount_id, | ||
direction: "buy", | ||
limit_price: 1310, | ||
amount: 100, | ||
signature_expiry_sec: Math.floor(Date.now() / 1000 + 300), | ||
max_fee: "0.01", | ||
nonce: Number(`${Date.now()}${Math.round(Math.random() * 999)}`), // LYRA nonce format: ${CURRENT UTC MS +/- 1 day}${RANDOM 3 DIGIT NUMBER} | ||
signer: wallet.address, | ||
order_type: "limit", | ||
mmp: false, | ||
signature: "filled_in_below" | ||
}; | ||
} | ||
|
||
function encodeTradeData(order){ | ||
let encoded_data = encoder.encode( // same as "encoded_data" in public/order_debug | ||
['address', 'uint', 'int', 'int', 'uint', 'uint', 'bool'], | ||
[ | ||
ASSET_ADDRESS, | ||
OPTION_SUB_ID, | ||
ethers.parseUnits(order.limit_price.toString(), 18), | ||
ethers.parseUnits(order.amount.toString(), 18), | ||
ethers.parseUnits(order.max_fee.toString(), 18), | ||
order.subaccount_id, order.direction === 'buy'] | ||
); | ||
return ethers.keccak256(Buffer.from(encoded_data.slice(2), 'hex')) // same as "encoded_data_hashed" in public/order_debug | ||
} | ||
|
||
async function signOrder(order) { | ||
const tradeModuleData = encodeTradeData(order) | ||
|
||
const action_hash = ethers.keccak256( | ||
encoder.encode( | ||
['bytes32', 'uint256', 'uint256', 'address', 'bytes32', 'uint256', 'address', 'address'], | ||
[ | ||
ACTION_TYPEHASH, | ||
order.subaccount_id, | ||
order.nonce, | ||
TRADE_MODULE_ADDRESS, | ||
tradeModuleData, | ||
order.signature_expiry_sec, | ||
wallet.address, | ||
order.signer | ||
] | ||
) | ||
); // same as "action_hash" in public/order_debug | ||
|
||
order.signature = wallet.signingKey.sign( | ||
ethers.keccak256(Buffer.concat([ | ||
Buffer.from("1901", "hex"), | ||
Buffer.from(DOMAIN_SEPARATOR.slice(2), "hex"), | ||
Buffer.from(action_hash.slice(2), "hex") | ||
])) // same as "typed_data_hash" in public/order_debug | ||
).serialized; | ||
} | ||
|
||
async function submitOrder(order, ws) { | ||
return new Promise((resolve, reject) => { | ||
const id = Math.floor(Math.random() * 1000); | ||
ws.send(JSON.stringify({ | ||
method: 'private/order', | ||
params: order, | ||
id: id | ||
})); | ||
|
||
ws.on('message', (message) => { | ||
const msg = JSON.parse(message); | ||
if (msg.id === id) { | ||
console.log('Got order response:', msg); | ||
resolve(msg); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
async function completeOrder() { | ||
const ws = await connectWs(); | ||
await loginClient(ws); | ||
const order = defineOrder(); | ||
await signOrder(order); | ||
console.log('Submitting order:', order); | ||
await submitOrder(order, ws); | ||
} | ||
|
||
completeOrder(); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
const {ethers, Contract} = require("ethers"); | ||
|
||
const axios = require('axios'); | ||
const dotenv = require('dotenv'); | ||
|
||
|
||
function getUTCEpochSec() { | ||
return Math.floor(Date.now() / 1000); | ||
} | ||
|
||
dotenv.config(); | ||
|
||
// Environment variables, double check these in the docs constants section | ||
const PRIVATE_KEY = process.env.OWNER_PRIVATE_KEY | ||
const PROVIDER_URL = 'https://l2-prod-testnet-0eakp60405.t.conduit.xyz' | ||
const USDC_ADDRESS = '0xe80F2a02398BBf1ab2C9cc52caD1978159c215BD' | ||
const DEPOSIT_MODULE_ADDRESS = '0xB430F3AE49f9d7a9B93fCCb558424972c385Cc38' | ||
const CASH_ADDRESS = '0xb8a082B53BdCBFB7c44C8Baf2F924096711EADcA' | ||
const ACTION_TYPEHASH = '0x4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17' | ||
const STANDARD_RISK_MANAGER_ADDRESS = '0x089fde8A32CD4Ef8D9F69DAed1B4CD5aC67d1ed7' | ||
const DOMAIN_SEPARATOR = '0xff2ba7c8d1c63329d3c2c6c9c19113440c004c51fe6413f65654962afaff00f3' | ||
|
||
// Ethers setup | ||
const PROVIDER = new ethers.JsonRpcProvider(PROVIDER_URL); | ||
const wallet = new ethers.Wallet(PRIVATE_KEY, PROVIDER); | ||
const encoder = ethers.AbiCoder.defaultAbiCoder(); | ||
|
||
const depositAmount = "10000"; | ||
const subaccountId = 0; // 0 For a new account | ||
|
||
async function approveUSDCForDeposit(wallet, amount) { | ||
const USDCcontract = new ethers.Contract( | ||
USDC_ADDRESS, | ||
["function approve(address _spender, uint256 _value) public returns (bool success)"], | ||
wallet | ||
); | ||
const nonce = await wallet.provider?.getTransactionCount(wallet.address, "pending"); | ||
await USDCcontract.approve(DEPOSIT_MODULE_ADDRESS, ethers.parseUnits(amount, 6), { | ||
gasLimit: 1000000, | ||
nonce: nonce | ||
}); | ||
} | ||
|
||
function encodeDepositData(amount){ | ||
let encoded_data = encoder.encode( // same as "encoded_data" in public/create_subaccount_debug | ||
['uint256', 'address', 'address'], | ||
[ | ||
ethers.parseUnits(amount, 6), | ||
CASH_ADDRESS, | ||
STANDARD_RISK_MANAGER_ADDRESS | ||
] | ||
); | ||
return ethers.keccak256(Buffer.from(encoded_data.slice(2), 'hex')) // same as "encoded_data_hashed" in public/create_subaccount_debug | ||
} | ||
|
||
function generateSignature(subaccountId, encodedData, expiry, nonce) { | ||
const action_hash = ethers.keccak256( // same as "action_hash" in public/create_subaccount_debug | ||
encoder.encode( | ||
['bytes32', 'uint256', 'uint256', 'address', 'bytes32', 'uint256', 'address', 'address'], | ||
[ | ||
ACTION_TYPEHASH, | ||
subaccountId, | ||
nonce, | ||
DEPOSIT_MODULE_ADDRESS, | ||
encodedData, | ||
expiry, | ||
wallet.address, | ||
wallet.address | ||
] | ||
) | ||
); | ||
|
||
const typed_data_hash = ethers.keccak256( // same as "typed_data_hash" in public/create_subaccount_debug | ||
Buffer.concat([ | ||
Buffer.from("1901", "hex"), | ||
Buffer.from(DOMAIN_SEPARATOR.slice(2), "hex"), | ||
Buffer.from(action_hash.slice(2), "hex"), | ||
]) | ||
); | ||
|
||
return wallet.signingKey.sign(typed_data_hash).serialized | ||
} | ||
|
||
async function signAuthenticationHeader() { | ||
const timestamp = Date.now().toString(); | ||
const signature = await wallet.signMessage(timestamp); | ||
return { | ||
"X-LyraWallet": wallet.address, | ||
"X-LyraTimestamp": timestamp, | ||
"X-LyraSignature": signature | ||
}; | ||
} | ||
|
||
async function createSubaccount() { | ||
// An action nonce is used to prevent replay attacks | ||
// LYRA nonce format: ${CURRENT UTC MS +/- 1 day}${RANDOM 3 DIGIT NUMBER} | ||
const nonce = Number(`${Date.now()}${Math.round(Math.random() * 999)}`); | ||
const expiry = getUTCEpochSec() + 300; // 5 min | ||
|
||
const encoded_data_hashed = encodeDepositData(depositAmount); // same as "encoded_data_hashed" in public/create_subaccount_debug | ||
const depositSignature = generateSignature(subaccountId, encoded_data_hashed, expiry, nonce); | ||
const authHeader = await signAuthenticationHeader(); | ||
|
||
await approveUSDCForDeposit(wallet, depositAmount); | ||
|
||
try { | ||
const response = await axios.request({ | ||
method: "POST", | ||
url: "https://api-demo.lyra.finance/private/create_subaccount", | ||
data: { | ||
margin_type: "SM", | ||
wallet: wallet.address, | ||
signer: wallet.address, | ||
nonce: nonce, | ||
amount: depositAmount, | ||
signature: depositSignature, | ||
signature_expiry_sec: expiry, | ||
asset_name: 'USDC', | ||
}, | ||
headers: authHeader, | ||
}); | ||
|
||
console.log(JSON.stringify(response.data, null, '\t')); | ||
} catch (error) { | ||
console.error("Error depositing to subaccount:", error); | ||
} | ||
} | ||
|
||
createSubaccount(); | ||
|
Oops, something went wrong.