diff --git a/evm/hardhat.config.ts b/evm/hardhat.config.ts index d1d1fafc6..9965d1a7c 100644 --- a/evm/hardhat.config.ts +++ b/evm/hardhat.config.ts @@ -131,6 +131,36 @@ task("deploy-token-impl", "Deploys the BridgeToken implementation").setAction(as ) }) +task("deploy-token-proxy", "Deploy BridgeToken behind a UUPS proxy") + .addParam("name", "Token name") + .addParam("symbol", "Token symbol") + .addOptionalParam("decimals", "Token decimals", "18") + .setAction(async (args, hre) => { + const { ethers, upgrades } = hre + + const name = String(args.name) + const symbol = String(args.symbol) + const decimals = Number(args.decimals) + + const [deployer] = await ethers.getSigners() + console.log("Deployer:", deployer.address) + console.log("Params:", { name, symbol, decimals }) + + const BridgeToken = await ethers.getContractFactory("BridgeToken") + + const proxy = await upgrades.deployProxy(BridgeToken, [name, symbol, decimals], { + initializer: "initialize", + }) + + await proxy.waitForDeployment() + const proxyAddress = await proxy.getAddress() + + const implAddress = await upgrades.erc1967.getImplementationAddress(proxyAddress) + + console.log("Proxy deployed to:", proxyAddress) + console.log("Implementation:", implAddress) + }) + task("upgrade-bridge-token", "Upgrades a BridgeToken to a new implementation") .addParam("factory", "The address of the OmniBridge contract") .addParam("nearTokenAccount", "The NEAR token ID") @@ -301,6 +331,13 @@ const config: HardhatUserConfig = { url: `https://polygon-mainnet.infura.io/v3/${INFURA_API_KEY}`, accounts: [`${EVM_PRIVATE_KEY}`], }, + hyperEvmMainnet: { + wormholeAddress: "0x7C0faFc4384551f063e05aee704ab943b8B53aB3", + omniChainId: 9, + chainId: 999, + url: "https://rpc.hyperliquid.xyz/evm", + accounts: [`${EVM_PRIVATE_KEY}`], + }, sepolia: { omniChainId: 0, chainId: 11155111, @@ -335,6 +372,13 @@ const config: HardhatUserConfig = { url: `https://polygon-amoy.infura.io/v3/${INFURA_API_KEY}`, accounts: [`${EVM_PRIVATE_KEY}`], }, + hyperEvmTestnet: { + wormholeAddress: "0xBB73cB66C26740F31d1FabDC6b7A46a038A300dd", + omniChainId: 9, + chainId: 998, + url: "https://rpc.hyperliquid-testnet.xyz/evm", + accounts: [`${EVM_PRIVATE_KEY}`], + }, }, etherscan: { apiKey: ETHERSCAN_API_KEY, diff --git a/evm/utils/hyperliquid/hl_spot_send.py b/evm/utils/hyperliquid/hl_spot_send.py new file mode 100644 index 000000000..f1fb3d663 --- /dev/null +++ b/evm/utils/hyperliquid/hl_spot_send.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +HyperLiquid spotSend — free spot token transfer on HyperCore. + +Uses the EXACT same signing code as the SDK (copied verbatim from signing.py). +If this works (returns correct wallet address in error), the signing setup is +correct and the issue is only in the EIP-712 types for sendToEvmWithData. + +Usage: + python3 hl_spot_send.py \ + --token "USDC:0x6d1e7cde53ba9467b783cb7c530ce054" \ + --amount "1.0" \ + --to 0xRECIPIENT + + Optional: + --testnet (use testnet instead of mainnet) + +Environment: + PRIVATE_KEY — your wallet private key (required) +""" + +import os +import sys +import time +import json +import argparse +import requests + +from eth_account import Account +from eth_account.messages import encode_typed_data +from eth_utils import to_hex + +API_URL_MAINNET = "https://api.hyperliquid.xyz/exchange" +API_URL_TESTNET = "https://api.hyperliquid-testnet.xyz/exchange" + +# Copied verbatim from SDK signing.py +SPOT_TRANSFER_SIGN_TYPES = [ + {"name": "hyperliquidChain", "type": "string"}, + {"name": "destination", "type": "string"}, + {"name": "token", "type": "string"}, + {"name": "amount", "type": "string"}, + {"name": "time", "type": "uint64"}, +] + +PRIMARY_TYPE = "HyperliquidTransaction:SpotSend" + + +def sign_inner(wallet, data: dict) -> dict: + """Copied verbatim from SDK signing.py""" + structured_data = encode_typed_data(full_message=data) + signed = wallet.sign_message(structured_data) + return {"r": to_hex(signed["r"]), "s": to_hex(signed["s"]), "v": signed["v"]} + + +def user_signed_payload(primary_type: str, payload_types: list, action: dict) -> dict: + """Copied verbatim from SDK signing.py""" + chain_id = int(action["signatureChainId"], 16) + return { + "domain": { + "name": "HyperliquidSignTransaction", + "version": "1", + "chainId": chain_id, + "verifyingContract": "0x0000000000000000000000000000000000000000", + }, + "types": { + primary_type: payload_types, + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + {"name": "verifyingContract", "type": "address"}, + ], + }, + "primaryType": primary_type, + "message": action, + } + + +def sign_user_signed_action(wallet, action: dict, payload_types: list, + primary_type: str, is_mainnet: bool) -> dict: + """Copied verbatim from SDK signing.py""" + action["signatureChainId"] = "0x66eee" + action["hyperliquidChain"] = "Mainnet" if is_mainnet else "Testnet" + data = user_signed_payload(primary_type, payload_types, action) + return sign_inner(wallet, data) + + +def send_request(action: dict, signature: dict, nonce: int, is_mainnet: bool): + url = API_URL_MAINNET if is_mainnet else API_URL_TESTNET + payload = { + "action": action, + "nonce": nonce, + "signature": signature, + } + print("\nRequest payload:") + print(json.dumps(payload, indent=2, default=str)) + print(f"\nPOST {url}") + + resp = requests.post(url, json=payload, timeout=10) + print(f"Response [{resp.status_code}]:") + print(json.dumps(resp.json(), indent=2)) + return resp.json() + + +def main(): + parser = argparse.ArgumentParser(description="HyperLiquid spotSend CLI") + parser.add_argument("--token", required=True, + help='Token, e.g. "USDC:0x6d1e7cde53ba9467b783cb7c530ce054"') + parser.add_argument("--amount", required=True, + help='Amount as string, e.g. "1.0"') + parser.add_argument("--to", required=True, + help="Destination address") + parser.add_argument("--testnet", action="store_true", + help="Use testnet instead of mainnet") + args = parser.parse_args() + + is_mainnet = not args.testnet + + private_key = os.environ.get("PRIVATE_KEY") + if not private_key: + print("Error: PRIVATE_KEY environment variable is not set") + sys.exit(1) + + wallet = Account.from_key(private_key) + nonce = int(time.time() * 1000) + + print(f"Wallet: {wallet.address}") + print(f"Network: {'Mainnet' if is_mainnet else 'Testnet'}") + print(f"Token: {args.token}") + print(f"Amount: {args.amount}") + print(f"To: {args.to}") + print(f"Nonce: {nonce}") + + action = { + "type": "spotSend", + "destination": args.to, + "token": args.token, + "amount": args.amount, + "time": nonce, + } + + signature = sign_user_signed_action( + wallet, action, SPOT_TRANSFER_SIGN_TYPES, PRIMARY_TYPE, is_mainnet, + ) + + send_request(action, signature, nonce, is_mainnet) + + +if __name__ == "__main__": + main() diff --git a/evm/utils/hyperliquid/hl_transfer.py b/evm/utils/hyperliquid/hl_transfer.py new file mode 100644 index 000000000..37fb11c2f --- /dev/null +++ b/evm/utils/hyperliquid/hl_transfer.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +HyperLiquid sendToEvmWithData script + +Transfers a token from HyperCore to an EVM contract with custom data payload. +The receiving contract must implement the ICoreReceiveWithData interface. + +Usage: + python3 hl_transfer.py \ + --token "USDC:0x6d1e7cde53ba9467b783cb7c530ce054" \ + --amount "10.0" \ + --to 0xRECIPIENT_CONTRACT \ + --chain-id 42161 \ + --data 0x + + Optional: + --source-dex "" (default: "") + --gas-limit 200000 (default: 200000) + --encoding hex (default: "hex") + --testnet (use testnet instead of mainnet) + +Environment: + PRIVATE_KEY — your wallet private key (required) + +Install deps: + pip install eth-account requests eth-utils +""" + +import os +import sys +import time +import json +import argparse +import requests + +from eth_account import Account +from eth_account.messages import encode_typed_data +from eth_utils import to_hex + +API_URL_MAINNET = "https://api.hyperliquid.xyz/exchange" +API_URL_TESTNET = "https://api.hyperliquid-testnet.xyz/exchange" + +SIGNATURE_CHAIN_ID = "0x66eee" + +SEND_TO_EVM_WITH_DATA_TYPES = [ + {"name": "hyperliquidChain", "type": "string"}, + {"name": "token", "type": "string"}, + {"name": "amount", "type": "string"}, + {"name": "sourceDex", "type": "string"}, + {"name": "destinationRecipient","type": "string"}, + {"name": "addressEncoding", "type": "string"}, + {"name": "destinationChainId", "type": "uint32"}, + {"name": "gasLimit", "type": "uint64"}, + {"name": "data", "type": "bytes"}, + {"name": "nonce", "type": "uint64"}, +] + +PRIMARY_TYPE = "HyperliquidTransaction:SendToEvmWithData" + + +def sign_inner(wallet, data: dict) -> dict: + """Verbatim from SDK signing.py""" + structured_data = encode_typed_data(full_message=data) + signed = wallet.sign_message(structured_data) + return {"r": to_hex(signed["r"]), "s": to_hex(signed["s"]), "v": signed["v"]} + + +def user_signed_payload(primary_type: str, payload_types: list, action: dict) -> dict: + """Verbatim from SDK signing.py""" + chain_id = int(action["signatureChainId"], 16) + return { + "domain": { + "name": "HyperliquidSignTransaction", + "version": "1", + "chainId": chain_id, + "verifyingContract": "0x0000000000000000000000000000000000000000", + }, + "types": { + primary_type: payload_types, + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + {"name": "verifyingContract", "type": "address"}, + ], + }, + "primaryType": primary_type, + "message": action, + } + + +def sign_user_signed_action(wallet, action: dict, payload_types: list, + primary_type: str, is_mainnet: bool) -> dict: + """Verbatim from SDK signing.py""" + action["signatureChainId"] = SIGNATURE_CHAIN_ID + action["hyperliquidChain"] = "Mainnet" if is_mainnet else "Testnet" + data = user_signed_payload(primary_type, payload_types, action) + return sign_inner(wallet, data) + + +def send_request(action: dict, signature: dict, nonce: int, is_mainnet: bool): + url = API_URL_MAINNET if is_mainnet else API_URL_TESTNET + payload = { + "action": action, + "nonce": nonce, + "signature": signature, + } + print("\nRequest payload:") + print(json.dumps(payload, indent=2, default=str)) + print(f"\nPOST {url}") + + resp = requests.post(url, json=payload, timeout=10) + print(f"Response [{resp.status_code}]:") + print(json.dumps(resp.json(), indent=2)) + return resp.json() + + +def main(): + parser = argparse.ArgumentParser( + description="HyperLiquid sendToEvmWithData CLI", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("--token", required=True, + help='Token, e.g. "USDC:0x6d1e7cde53ba9467b783cb7c530ce054"') + parser.add_argument("--amount", required=True, + help='Amount as string, e.g. "10.0"') + parser.add_argument("--to", required=True, + help="Destination contract address (destinationRecipient)") + parser.add_argument("--chain-id", type=int, default=3, + help="Destination chain ID (default: 3 Arbitrum)") + parser.add_argument("--data", default="0x", + help="Custom data payload hex (default: 0x)") + parser.add_argument("--source-dex", default="", + help="Source DEX name (default: \"\")") + parser.add_argument("--gas-limit", type=int, default=200000, + help="Gas limit for coreReceiveWithData call (default: 200000)") + parser.add_argument("--encoding", default="hex", choices=["hex", "base58"], + help="Address encoding (default: hex)") + parser.add_argument("--testnet", action="store_true", + help="Use testnet instead of mainnet") + + args = parser.parse_args() + is_mainnet = not args.testnet + + private_key = os.environ.get("PRIVATE_KEY") + if not private_key: + print("Error: PRIVATE_KEY environment variable is not set") + print(" export PRIVATE_KEY=0x...") + sys.exit(1) + + wallet = Account.from_key(private_key) + nonce = int(time.time() * 1000) + + print(f"Wallet: {wallet.address}") + print(f"Network: {'Mainnet' if is_mainnet else 'Testnet'}") + print(f"Token: {args.token}") + print(f"Amount: {args.amount}") + print(f"Recipient: {args.to}") + print(f"Chain ID: {args.chain_id}") + print(f"Source DEX: {args.source_dex}") + print(f"Gas limit: {args.gas_limit}") + print(f"Data: {args.data}") + print(f"Nonce: {nonce}") + + data_bytes = bytes.fromhex(args.data.removeprefix("0x")) + + action = { + "type": "sendToEvmWithData", + "token": args.token, + "amount": args.amount, + "sourceDex": args.source_dex, + "destinationRecipient":args.to, + "addressEncoding": args.encoding, + "destinationChainId": args.chain_id, + "gasLimit": args.gas_limit, + "data": data_bytes, + "nonce": nonce, + } + + signature = sign_user_signed_action( + wallet, action, SEND_TO_EVM_WITH_DATA_TYPES, PRIMARY_TYPE, is_mainnet, + ) + + action["data"] = args.data + + send_request(action, signature, nonce, is_mainnet) + + +if __name__ == "__main__": + main() diff --git a/evm/utils/hyperliquid/link_tokens.py b/evm/utils/hyperliquid/link_tokens.py new file mode 100644 index 000000000..15eec1fd7 --- /dev/null +++ b/evm/utils/hyperliquid/link_tokens.py @@ -0,0 +1,99 @@ +from typing import TypedDict, Literal, Union + +import requests +from eth_account import Account +from eth_account.signers.local import LocalAccount +from web3 import Web3 +from web3.middleware import SignAndSendRawMiddlewareBuilder +from hyperliquid.utils import constants +from hyperliquid.utils.signing import get_timestamp_ms, sign_l1_action + +CreateInputParams = TypedDict("CreateInputParams", {"nonce": int}) +CreateInput = TypedDict("CreateInput", {"create": CreateInputParams}) +FinalizeEvmContractInput = Union[Literal["firstStorageSlot"], CreateInput] +FinalizeEvmContractAction = TypedDict( + "FinalizeEvmContractAction", + {"type": Literal["finalizeEvmContract"], "token": int, "input": FinalizeEvmContractInput}, +) + +DEFAULT_CONTRACT_ADDRESS = Web3.to_checksum_address( + "0x2E98e98aB34b42b14FeC9d431F7B051B232Ba133" # change this to your contract address if you are skipping deploying +) +TOKEN = 1562 # note that if changing this you likely should also change the abi to have a different name and perhaps also different decimals and initial supply +PRIVATE_KEY = "0xPRIVATE_KEY" # Change this to your private key + +# Connect to the JSON-RPC endpoint +rpc_url = "https://rpc.hyperliquid-testnet.xyz/evm" + +contract_address = DEFAULT_CONTRACT_ADDRESS + +def requestEvmContract(account): + assert contract_address is not None + action = { + "type": "spotDeploy", + "requestEvmContract": { + "token": TOKEN, + "address": contract_address.lower(), + "evmExtraWeiDecimals": 10, + }, + } + nonce = get_timestamp_ms() + signature = sign_l1_action(account, action, None, nonce, None, False) + payload = { + "action": action, + "nonce": nonce, + "signature": signature, + "vaultAddress": None, + } + response = requests.post(constants.TESTNET_API_URL + "/exchange", json=payload) + print(response.json()) + +def finalizeEvmContract(account): + creation_nonce = 4 + print(creation_nonce) + use_create_finalization = True + finalize_action: FinalizeEvmContractAction + if use_create_finalization: + finalize_action = { + "type": "finalizeEvmContract", + "token": TOKEN, + "input": {"create": {"nonce": creation_nonce}}, + } + else: + finalize_action = {"type": "finalizeEvmContract", "token": TOKEN, "input": "firstStorageSlot"} + nonce = get_timestamp_ms() + signature = sign_l1_action(account, finalize_action, None, nonce, None, False) + payload = { + "action": finalize_action, + "nonce": nonce, + "signature": signature, + "vaultAddress": None, + } + response = requests.post(constants.TESTNET_API_URL + "/exchange", json=payload) + print(response.json()) + + +def main(): + w3 = Web3(Web3.HTTPProvider(rpc_url)) + + # The account will be used both for deploying the ERC20 contract and linking it to your native spot asset + # You can also switch this to create an account a different way if you don't want to include a secret key in code + if PRIVATE_KEY == "0xPRIVATE_KEY": + raise Exception("must set private key or create account another way") + account: LocalAccount = Account.from_key(PRIVATE_KEY) + print(f"Running with address {account.address}") + w3.middleware_onion.add(SignAndSendRawMiddlewareBuilder.build(account)) + w3.eth.default_account = account.address + # Verify connection + if not w3.is_connected(): + raise Exception("Failed to connect to the Ethereum network") + + print(TOKEN, contract_address.lower()) + #requestEvmContract(account) + finalizeEvmContract(account) + +if __name__ == "__main__": + main() + +# curl -s https://api.hyperliquid-testnet.xyz/info -H "Content-Type: application/json" -d '{"type": "tokenDetails", "tokenId": "0x646586ef3576346a4fcc9548909c1cba"}' | jq +# curl -s https://api.hyperliquid-testnet.xyz/info -H "Content-Type: application/json" -d '{ "type": "spotMeta" }' | jq '.tokens[] | select(.name=="JHWL")' diff --git a/evm/utils/hyperliquid/spot_deploy.py b/evm/utils/hyperliquid/spot_deploy.py new file mode 100644 index 000000000..e33415dd9 --- /dev/null +++ b/evm/utils/hyperliquid/spot_deploy.py @@ -0,0 +1,123 @@ +import getpass +import json +import os + +import eth_account +from eth_account.signers.local import LocalAccount + +from hyperliquid.exchange import Exchange +from hyperliquid.info import Info +from hyperliquid.utils import constants + +def setup(base_url=None, skip_ws=False, perp_dexs=None): + config_path = os.path.join(os.path.dirname(__file__), "config.json") + with open(config_path) as f: + config = json.load(f) + account: LocalAccount = eth_account.Account.from_key(get_secret_key(config)) + address = config["account_address"] + if address == "": + address = account.address + print("Running with account address:", address) + if address != account.address: + print("Running with agent address:", account.address) + info = Info(base_url, skip_ws, perp_dexs=perp_dexs) + user_state = info.user_state(address) + spot_user_state = info.spot_user_state(address) + margin_summary = user_state["marginSummary"] + if float(margin_summary["accountValue"]) == 0 and len(spot_user_state["balances"]) == 0: + print("Not running the example because the provided account has no equity.") + url = info.base_url.split(".", 1)[1] + error_string = f"No accountValue:\nIf you think this is a mistake, make sure that {address} has a balance on {url}.\nIf address shown is your API wallet address, update the config to specify the address of your account, not the address of the API wallet." + raise Exception(error_string) + exchange = Exchange(account, base_url, account_address=address, perp_dexs=perp_dexs) + return address, info, exchange + + +def get_secret_key(config): + return config["secret_key"] +def step1(exchange): + # Step 1: Registering the Token + # + # Takes part in the spot deploy auction and if successful, registers token "TEST0" + # with sz_decimals 2 and wei_decimals 8. + # The max gas is 10,000 HYPE and represents the max amount to be paid for the spot deploy auction. + register_token_result = exchange.spot_deploy_register_token("TEST0", 2, 8, 1000000000000, "Test token example") + print(register_token_result) + # If registration is successful, a token index will be returned. This token index is required for + # later steps in the spot deploy process. + if register_token_result["status"] == "ok": + token = register_token_result["response"]["data"] + else: + return + return token + +def step2(address, exchange, token): + # Step 2: User Genesis + # + # User genesis can be called multiple times to associate balances to specific users and/or + # tokens for genesis. + user_genesis_result = exchange.spot_deploy_user_genesis( + token, + [ + (address, "100000000900000000"), + ], + [], + ) + print(user_genesis_result) + +def step3(exchange, token): + # Step 3: Genesis + # + # Finalize genesis. The max supply of 300000000000000 wei needs to match the total + # allocation above from user genesis. + # + # "noHyperliquidity" can also be set to disable hyperliquidity. In that case, no balance + # should be associated with hyperliquidity from step 2 (user genesis). + genesis_result = exchange.spot_deploy_genesis(token, "100000000900000000", True) + print(genesis_result) + +def step4(exchange, token): + # Step 4: Register Spot + # + # Register the spot pair (TEST0/USDC) given base and quote token indices. 0 represents USDC. + # The base token is the first token in the pair and the quote token is the second token. + register_spot_result = exchange.spot_deploy_register_spot(token, 0) + print(register_spot_result) + # If registration is successful, a spot index will be returned. This spot index is required for + # registering hyperliquidity. + if register_spot_result["status"] == "ok": + spot = register_spot_result["response"]["data"] + else: + return + + return spot + +def step5(exchange, spot): + + # Step 5: Register Hyperliquidity + # + # Registers hyperliquidity for the spot pair. In this example, hyperliquidity is registered + # with a starting price of $2, an order size of 4, and 100 total orders. + # + # This step is required even if "noHyperliquidity" was set to True. + # If "noHyperliquidity" was set to True during step 3 (genesis), then "n_orders" is required to be 0. + register_hyperliquidity_result = exchange.spot_deploy_register_hyperliquidity(spot, 2.0, 4.0, 0, None) + print(register_hyperliquidity_result) + +def main(): + address, info, exchange = setup(constants.TESTNET_API_URL, skip_ws=True) + print(address, info, exchange) + + # token = step1() + # token = 1562 + # step2(address, exchange, token) + # step3(exchange, token) + # spot = step4(exchange, token) + # print(spot) + # spot = 1436 + # step5(exchange, spot) + +if __name__ == "__main__": + main() + +# curl -s https://api.hyperliquid-testnet.xyz/info -H "Content-Type: application/json" -d '{"type":"spotDeployState","user":"0x36279BeA31b1CC48dd4454a2C7149f331eF3f3c3"}' | jq