const { ethers } = require('ethers');
const crypto = require('crypto');
const fetch = require('node-fetch');
// API endpoints and contract addresses
const INFRA_BASE_URL = 'https://mainnetapiserverwriter.logx.network';
const INFRA_READER_URL = 'https://mainnetapiserver.logx.network';
const ENDPOINT_CONTRACT_ADDRESS = '0xBC87C2397601391E66adeC581786dF3F8eeE6124';
// Helper function to get subaccount ID
function getSubAccountId(address) {
const subAccountIdString = `1_${address}_1`;
const subAccountIdHash = subaccountIdToHex(subAccountIdString);
return { subAccountIdString, subAccountIdHash };
}
// Convert subaccount ID to hex format
function subaccountIdToHex(subaccountId) {
const bytes = subaccountIdToBytes32(subaccountId);
return '0x' + Buffer.from(bytes).toString('hex');
}
// Convert subaccount ID to bytes32 format
function subaccountIdToBytes32(id) {
const parts = id.split('_');
const brokerId = parseInt(parts[0], 10);
const ethAddress = parts[1];
const subaccountNum = parseInt(parts[2], 10);
// Validate inputs
if (isNaN(brokerId) || brokerId >= Math.pow(2, 48)) {
throw new Error(`Invalid broker ID: ${brokerId}`);
}
if (isNaN(subaccountNum) || subaccountNum >= Math.pow(2, 48)) {
throw new Error(`Invalid subaccount number: ${subaccountNum}`);
}
// Convert to bytes
const first6Bytes = uintTo6Bytes(brokerId);
const next20Bytes = ethAddressTo20Bytes(ethAddress);
const last6Bytes = uintTo6Bytes(subaccountNum);
return [...first6Bytes, ...next20Bytes, ...last6Bytes];
}
// Convert unsigned integer to 6 bytes
function uintTo6Bytes(num) {
if (num < 0 || num >= Math.pow(2, 48)) {
throw new Error(`number must be positive or less than 2^48: ${num}`);
}
const bytes = [0, 0, 0, 0, 0, 0];
for (let i = 0; i < 6; i++) {
bytes[5 - i] = num & 0xff;
num = num >> 8;
}
return bytes;
}
// Convert Ethereum address to 20 bytes
function ethAddressTo20Bytes(address) {
let cleanAddress = address;
if (address.startsWith('0x')) {
cleanAddress = address.slice(2);
}
const bytes = Buffer.from(cleanAddress, 'hex');
if (bytes.length !== 20) {
throw new Error(
`invalid address length: expected 20 bytes, got ${bytes.length} bytes`
);
}
return Array.from(bytes);
}
// Get the current nonce from the API
async function getNonce(address) {
const { subAccountIdHash } = getSubAccountId(address);
try {
const res = await fetch(
`${INFRA_READER_URL}/api/v1/subaccount/nonce/${subAccountIdHash}`,
{
headers: {
'Broker-Id': '1'
}
}
);
if (!res.ok) {
throw new Error('Failed to get nonce');
}
const data = await res.json();
return Number(data?.body?.nonce);
} catch (error) {
console.error('Error getting nonce:', error);
throw error;
}
}
// The definedTypes for EIP-712 signing
const definedTypes = {
Register: [
{ name: 'subAccountId', type: 'bytes32' },
{ name: 'userAddress', type: 'address' },
{ name: 'sessionKey', type: 'address' },
{ name: 'expiryTimeStamp', type: 'uint128' },
{ name: 'nonce', type: 'uint128' },
{ name: 'chainId', type: 'uint256' }
]
};
// Sign with ethers library
async function signWithEthers(privateKey, domain, message, primaryType) {
const signer = new ethers.Wallet(privateKey);
const typeDefinition = {
[primaryType]: definedTypes[primaryType]
};
const signature = await signer.signTypedData(domain, typeDefinition, message);
return signature;
}
/**
* Main authentication function
*
* @param {string} userAddress - Ethereum address to authenticate
* @param {string} userPrivateKey - Private key corresponding to userAddress
* @param {number} chainId - Chain ID (e.g., 42161 for Arbitrum One)
* @returns {Promise<object>} Authentication result with credentials
*/
async function registerAccount(userAddress, userPrivateKey, chainId) {
try {
// STEP 1: Generate session data
console.log('Generating session data...');
const signer = ethers.Wallet.createRandom();
const publicKey = signer.address;
const privateKey = signer.privateKey;
// STEP 2: Register account
console.log('Preparing registration data...');
const { subAccountIdString, subAccountIdHash } = getSubAccountId(userAddress);
const nonce = await getNonce(userAddress);
// Prepare the message to sign
const message = {
subAccountId: subAccountIdHash,
userAddress: userAddress,
sessionKey: publicKey,
expiryTimeStamp: new Date().getTime() + 6 * 24 * 60 * 60 * 1000, // 6 days
nonce: nonce,
chainId: chainId
};
// Prepare the domain
const domain = {
name: 'LogX',
version: '1',
chainId: chainId,
verifyingContract: ENDPOINT_CONTRACT_ADDRESS
};
// Sign with user's Ethereum private key
console.log('Signing with user private key...');
const userSigner = new ethers.Wallet(userPrivateKey);
const ethSignature = await userSigner.signTypedData(
domain,
{ Register: definedTypes.Register },
message
);
// Sign with session key
console.log('Signing with session key...');
const signingSignature = await signWithEthers(
privateKey,
domain,
message,
'Register'
);
// STEP 3: Send authentication request
console.log('Sending authentication request...');
const res = await fetch(`${INFRA_BASE_URL}/api/v1/auth`, {
method: 'POST',
body: JSON.stringify({
subaccountId: subAccountIdString,
signingKey: publicKey,
signingSignature: signingSignature,
ethAddress: userAddress,
ethSignature: ethSignature,
expiryTs: message.expiryTimeStamp,
nonce: message.nonce,
chainId
}),
headers: {
'Content-Type': 'application/json',
'broker-id': '1'
}
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(`Authentication failed: ${errorData.message || res.statusText}`);
}
// Process the response
const responseData = await res.json();
const account = {
...responseData.body, // Contains logx_key and logx_secret
sessionKey: privateKey,
publicKey: publicKey,
expiryTs: message.expiryTimeStamp - 0.5 * 24 * 60 * 60 * 1000 // Expires half a day earlier
};
console.log("Authentication successful!");
return account;
} catch (error) {
console.error("Authentication error:", error);
throw error;
}
}