Skip to content

Instantly share code, notes, and snippets.

@dome
Created December 17, 2025 11:13
Show Gist options
  • Select an option

  • Save dome/b97444a6fe6802063e543e02ed59e20b to your computer and use it in GitHub Desktop.

Select an option

Save dome/b97444a6fe6802063e543e02ed59e20b to your computer and use it in GitHub Desktop.
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