Created
December 17, 2025 11:13
-
-
Save dome/b97444a6fe6802063e543e02ed59e20b to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| require('dotenv').config(); | |
| const { ethers } = require('ethers'); | |
| // Configuration | |
| const CONFIG = { | |
| RPC_URL: process.env.RPC_URL || 'https://rpc.0xl3.com', | |
| PRIVATE_KEY: process.env.PRIVATE_KEY_EX, | |
| CHAIN_ID: 7117, | |
| }; | |
| // EIP-712 Domain and Types for GOLD token on 0XL3 | |
| const DOMAIN = { | |
| name: 'GOLD Token', | |
| version: '1', | |
| chainId: 7117, | |
| verifyingContract: '0xAc878E8d8A80750C0A7E955EfC320A16Be6ab69D' | |
| }; | |
| const TYPES = { | |
| ReceiveWithAuthorization: [ | |
| { name: 'from', type: 'address' }, | |
| { name: 'to', type: 'address' }, | |
| { name: 'value', type: 'uint256' }, | |
| { name: 'validAfter', type: 'uint256' }, | |
| { name: 'validBefore', type: 'uint256' }, | |
| { name: 'nonce', type: 'bytes32' } | |
| ] | |
| }; | |
| // Contract ABI | |
| const CONTRACT_ABI = [ | |
| "function name() view returns (string)", | |
| "function symbol() view returns (string)", | |
| "function balanceOf(address) view returns (uint256)", | |
| "function receiveWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)", | |
| "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)", | |
| "function authorizationState(address authorizer, bytes32 nonce) view returns (bool)", | |
| "event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)" | |
| ]; | |
| // Initialize provider and wallet | |
| let provider, wallet; | |
| // Signature verification function | |
| function verifySignature(authData) { | |
| try { | |
| const domain = { | |
| name: "GOLD Token", | |
| version: "1", | |
| chainId: CONFIG.CHAIN_ID, | |
| verifyingContract: authData.tokenContract | |
| }; | |
| const types = { | |
| ReceiveWithAuthorization: [ | |
| { name: "from", type: "address" }, | |
| { name: "to", type: "address" }, | |
| { name: "value", type: "uint256" }, | |
| { name: "validAfter", type: "uint256" }, | |
| { name: "validBefore", type: "uint256" }, | |
| { name: "nonce", type: "bytes32" } | |
| ] | |
| }; | |
| // For receiveWithAuthorization, to must be zero address in the signature | |
| const value = { | |
| from: authData.from, | |
| to: "0x0000000000000000000000000000000000000000", // Always zero address for receiveWithAuthorization | |
| value: authData.value, | |
| validAfter: authData.validAfter, | |
| validBefore: authData.validBefore, | |
| nonce: authData.nonce | |
| }; | |
| // Reconstruct signature | |
| const signature = ethers.Signature.from({ | |
| v: authData.v, | |
| r: authData.r, | |
| s: authData.s | |
| }); | |
| // Verify signature | |
| const digest = ethers.TypedDataEncoder.hash(domain, types, value); | |
| const recoveredAddress = ethers.recoverAddress(digest, signature); | |
| return recoveredAddress.toLowerCase() === authData.from.toLowerCase(); | |
| } catch (error) { | |
| console.error('Signature verification error:', error); | |
| return false; | |
| } | |
| } | |
| // Main transfer execution function | |
| async function executeTransfer(authData) { | |
| try { | |
| // Validate required fields | |
| const requiredFields = ['from', 'to', 'value', 'validAfter', 'validBefore', 'nonce', 'v', 'r', 's', 'tokenContract']; | |
| for (const field of requiredFields) { | |
| if (!authData[field]) { | |
| throw new Error(`Missing required field: ${field}`); | |
| } | |
| } | |
| // Validate tokenContract address format | |
| if (!ethers.isAddress(authData.tokenContract)) { | |
| throw new Error('Invalid tokenContract address format'); | |
| } | |
| // Initialize provider and wallet if not already done | |
| if (!provider || !wallet) { | |
| provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL); | |
| wallet = new ethers.Wallet(CONFIG.PRIVATE_KEY, provider); | |
| } | |
| // Create contract instance | |
| const contract = new ethers.Contract(authData.tokenContract, CONTRACT_ABI, wallet); | |
| console.log('π Processing transfer authorization:', { | |
| from: authData.from, | |
| to: authData.to, | |
| value: ethers.formatEther(authData.value), | |
| nonce: authData.nonce, | |
| tokenContract: authData.tokenContract | |
| }); | |
| // Verify signature | |
| if (!verifySignature(authData)) { | |
| throw new Error('Invalid signature'); | |
| } | |
| // Check if authorization is still valid | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if (currentTime < authData.validAfter) { | |
| throw new Error('Authorization not yet valid'); | |
| } | |
| if (currentTime > authData.validBefore) { | |
| throw new Error('Authorization expired'); | |
| } | |
| // Check if authorization has already been used | |
| const isUsed = await contract.authorizationState(authData.from, authData.nonce); | |
| if (isUsed) { | |
| throw new Error('Authorization already used'); | |
| } | |
| // Execute the transfer | |
| console.log('π Executing receiveWithAuthorization...'); | |
| const tx = await contract.receiveWithAuthorization( | |
| authData.from, | |
| authData.to, | |
| authData.value, | |
| authData.validAfter, | |
| authData.validBefore, | |
| authData.nonce, | |
| authData.v, | |
| authData.r, | |
| authData.s | |
| ); | |
| console.log('β³ Transaction submitted:', tx.hash); | |
| // Wait for confirmation | |
| const receipt = await tx.wait(); | |
| console.log('β Transaction confirmed:', { | |
| hash: receipt.hash, | |
| blockNumber: receipt.blockNumber, | |
| gasUsed: receipt.gasUsed.toString() | |
| }); | |
| return { | |
| success: true, | |
| transactionHash: receipt.hash, | |
| blockNumber: receipt.blockNumber, | |
| gasUsed: receipt.gasUsed.toString() | |
| }; | |
| } catch (error) { | |
| console.error('β Transfer execution error:', error); | |
| throw error; | |
| } | |
| } | |
| async function signAndTransfer() { | |
| try { | |
| // Check if private key is available | |
| if (!process.env.PRIVATE_KEY_EX) { | |
| console.error('β PRIVATE_KEY_EX not found in .env file'); | |
| return; | |
| } | |
| // Initialize wallet | |
| const wallet = new ethers.Wallet(process.env.PRIVATE_KEY_EX); | |
| console.log('π Wallet address:', wallet.address); | |
| // Generate random nonce (32 bytes) | |
| const nonce = ethers.randomBytes(32); | |
| console.log('π² Generated nonce:', ethers.hexlify(nonce)); | |
| // Set time validity (valid for 2 hours) | |
| const now = Math.floor(Date.now() / 1000); | |
| const validAfter = now - 60; // Start 1 minute ago to avoid timing issues | |
| const validBefore = now + (2 * 60 * 60); // 2 hours | |
| // Transfer data for receiveWithAuthorization (to must be zero address in signature) | |
| const transferData = { | |
| from: wallet.address, | |
| to: '0x0000000000000000000000000000000000000000', // Must be zero address for receiveWithAuthorization | |
| value: ethers.parseUnits('1', 18).toString(), // 1 GOLD token | |
| validAfter: validAfter, | |
| validBefore: validBefore, | |
| nonce: ethers.hexlify(nonce) | |
| }; | |
| console.log('π Transfer data:'); | |
| console.log(' From:', transferData.from); | |
| console.log(' To:', transferData.to, '(zero address for receiveWithAuthorization)'); | |
| console.log(' Value:', transferData.value, '(1 GOLD token)'); | |
| console.log(' Valid after:', new Date(validAfter * 1000).toISOString()); | |
| console.log(' Valid before:', new Date(validBefore * 1000).toISOString()); | |
| console.log(' Recipient (msg.sender):', wallet.address); | |
| // Sign the message using EIP-712 | |
| console.log('βοΈ Signing message...'); | |
| const signature = await wallet.signTypedData(DOMAIN, TYPES, transferData); | |
| console.log('β Signature:', signature); | |
| // Split signature into v, r, s | |
| const sig = ethers.Signature.from(signature); | |
| // Prepare authorization data | |
| // Note: receiveWithAuthorization transfers to msg.sender (caller), not to address in params | |
| const authData = { | |
| from: transferData.from, | |
| to: wallet.address, // The actual recipient will be msg.sender (this wallet) | |
| value: transferData.value, | |
| validAfter: transferData.validAfter, | |
| validBefore: transferData.validBefore, | |
| nonce: transferData.nonce, // Keep as hex string | |
| tokenContract: DOMAIN.verifyingContract, | |
| v: sig.v, | |
| r: sig.r, | |
| s: sig.s | |
| }; | |
| console.log('π Executing transfer...'); | |
| const result = await executeTransfer(authData); | |
| if (result.success) { | |
| console.log('β Transfer successful!'); | |
| console.log('π Transaction hash:', result.transactionHash); | |
| console.log('π View on explorer: https://exp.0xl3.com/tx/' + result.transactionHash); | |
| } else { | |
| console.log('β Transfer failed:', result.error); | |
| } | |
| } catch (error) { | |
| console.error('β Error:', error.message); | |
| if (error.code) { | |
| console.error(' Code:', error.code); | |
| } | |
| if (error.reason) { | |
| console.error(' Reason:', error.reason); | |
| } | |
| } | |
| } | |
| // Run the function | |
| if (require.main === module) { | |
| signAndTransfer(); | |
| } | |
| module.exports = { signAndTransfer }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment