Created
April 18, 2024 22:20
-
-
Save losh11/fede191c14a654338771a66b13902363 to your computer and use it in GitHub Desktop.
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
/* eslint-disable radix */ | |
import BigNumber from 'bignumber.js'; | |
import BIP32Factory, { BIP32Interface } from 'bip32'; | |
import bs58check from 'bs58check'; | |
import * as ecc from 'tiny-secp256k1'; | |
import { Buffer } from 'buffer'; | |
import crypto from 'crypto'; | |
import { getHarmony } from '@/api/base/apiFactory'; | |
import { BaseFeeOption, FeeOption } from '@/api/types'; | |
import { RealmToken } from '@/realm/tokens'; | |
import { StringNumber } from '../../../entities'; | |
import { | |
BlockExplorer, | |
ExtendedPublicKeyAndChainCode, | |
NativeTokenSymbol, | |
Network, | |
NetworkIcon, | |
PreparedTransaction, | |
TotalFee, | |
WalletData, | |
WalletDataWithSeed, | |
} from './base'; | |
import { HarmonyTransport } from './HarmonyTransport'; | |
import { ChainAgnostic } from './utils/ChainAgnostic'; | |
import CompactSize from './utils/CompactSize'; | |
import { WalletStorage } from './walletState'; | |
import loc from '/loc'; | |
const bip32 = BIP32Factory(ecc); | |
const RIPEMD160 = require('ripemd160'); | |
const secp256k1 = require('secp256k1'); | |
type SendRequest = { | |
amount: bigint; | |
to: string; | |
}; | |
type UTXOIn = { | |
previousOutput: { | |
hash: string; | |
index: number; | |
}; | |
sequence: number; | |
script: Buffer; | |
signature?: Buffer; | |
signatureSize?: Buffer; | |
}; | |
type LitecoinTransaction = { | |
version: 1; | |
txIns: UTXOIn[]; | |
txOuts: { | |
value: bigint; | |
pkScript: Buffer; | |
pkScriptSize: number; | |
}[]; | |
locktime: 0; | |
}; | |
function newTx(): LitecoinTransaction { | |
return { | |
version: 1, | |
txIns: [], | |
txOuts: [], | |
locktime: 0, | |
}; | |
} | |
const NETWORK_BYTE = '30'; | |
const WALLET = { | |
wif: 0xb0, | |
bip32: { | |
public: 0x0488b2e4, | |
private: 0x0488ade4, | |
}, | |
}; | |
export class LitecoinNetwork implements Network { | |
label = loc.network.litecoin; | |
caipId: string = ChainAgnostic.NETWORK_LITECOIN; | |
nativeTokenCaipId: string = ChainAgnostic.COIN_LITECOIN; | |
nativeTokenDecimals: number = 8; | |
nativeTokenSymbol: NativeTokenSymbol = 'LTC'; | |
paymentUriPrefix = 'litecoin'; | |
blockExplorer: BlockExplorer = { | |
transactionUri(txId: string) { | |
return `https://litecoinspace.org/tx/${txId}`; | |
}, | |
}; | |
icon: NetworkIcon = ({ opacity }) => ({ | |
id: 'ltc', | |
fgColor: '#a6a9aa', | |
bgColor: `rgba(77, 77, 78, ${opacity})`, | |
}); | |
async createPaymentTransaction(data: WalletData, to: string, amount: StringNumber): Promise<SendRequest> { | |
return { | |
to, | |
amount: BigInt(amount), | |
}; | |
} | |
createTokenTransferTransaction(_data: WalletData, _to: string, _token: RealmToken, _amount: StringNumber): Promise<SendRequest> { | |
throw new Error('not supported'); | |
} | |
async deriveAddress(wallet: WalletData): Promise<string> { | |
return this._getAddressByIndexAndChange(wallet, 0, false); | |
} | |
async deriveAllAddresses(wallet: WalletData): Promise<string[]> { | |
return [await this.deriveAddress(wallet)]; | |
} | |
getDerivationPath(accountIdx?: number): string { | |
return `m/44'/2'/${accountIdx ?? 0}'`; | |
} | |
isAddressValid(address: string): boolean { | |
try { | |
serializePayToPubkeyHashScript(address); | |
} catch (_) { | |
return false; | |
} | |
return !!address; | |
} | |
private async getPrivateKey(wallet: WalletDataWithSeed, index: number, change: number) { | |
const path = this.getDerivationPath(wallet.accountIdx) + '/' + change + '/' + index; | |
const root = bip32.fromSeed(Buffer.from(wallet.seed.data), WALLET); | |
return root.derivePath(path); | |
} | |
getExtendedPublicKey(seed: ArrayBuffer, accountIdx?: number): ExtendedPublicKeyAndChainCode { | |
const path = this.getDerivationPath(accountIdx); | |
const root = bip32.fromSeed(Buffer.from(seed), WALLET); | |
const bip32Interface = root.derivePath(path); | |
return { | |
extendedPublicKey: bip32Interface.publicKey, | |
chainCode: bip32Interface.chainCode, | |
}; | |
} | |
async signTransaction(data: WalletDataWithSeed, transaction: LitecoinTransaction): Promise<string> { | |
const key = await this.getPrivateKey(data, 0, 0); | |
for (const txInIndex in transaction.txIns) { | |
const { signature, publicKey } = await signTransaction(transaction, key, parseInt(txInIndex), 1); | |
transaction = addP2KHSignature(transaction, signature, publicKey, parseInt(txInIndex)); | |
} | |
return serializeTransaction(transaction).toString('hex'); | |
} | |
private _getAddressByIndexAndChange(wallet: WalletData, index: number, isChangeAddress = false) { | |
const path = (isChangeAddress ? '1' : '0') + '/' + index; | |
const root = deriveRoot(wallet); | |
const child = root.derivePath(path); | |
return pubkeyToAddress(child.publicKey, NETWORK_BYTE); | |
} | |
} | |
export function deriveRoot(wallet: WalletData) { | |
const publicKey = Buffer.from(wallet.extendedPublicKey); | |
if (wallet.chainCode) { | |
const chainCode = Buffer.from(wallet.chainCode); | |
return bip32.fromPublicKey(publicKey, chainCode); | |
} else { | |
throw new Error('[litecoin] missing chainCode in wallet data'); | |
} | |
} | |
function serializePayToPubkeyHashScript(address: string): Buffer { | |
const decodedAddress = bs58check.decode(address).slice(1); | |
return Buffer.from('76a914' + decodedAddress.toString('hex') + '88ac', 'hex'); | |
} | |
function pubkeyToAddress(pubkey: Buffer, networkByte: any) { | |
let hash = crypto.createHash('sha256').update(pubkey).digest(); | |
const pubKeyHash = new RIPEMD160().update(hash).digest(); | |
networkByte = Buffer.from(networkByte, 'hex'); | |
return bs58check.encode(Buffer.concat([networkByte, pubKeyHash])); | |
} | |
export class LitecoinTransport extends HarmonyTransport<unknown, unknown, unknown> { | |
async prepareTransaction( | |
network: LitecoinNetwork, | |
walletData: WalletData, | |
store: WalletStorage<unknown>, | |
tx: SendRequest, | |
fee: BaseFeeOption, | |
): Promise<PreparedTransaction<unknown>> { | |
const singleAddress = await network.deriveAddress(walletData); | |
const balances = await this.fetchBalance(network, walletData); | |
const balance = BigInt(balances.find(item => item.balance.token === network.nativeTokenCaipId)?.balance?.value ?? 0); | |
let transaction = newTx(); | |
transaction.txIns = await this.fetchUtxoFromHarmony(singleAddress, ChainAgnostic.NETWORK_LITECOIN); | |
let pkScript = serializePayToPubkeyHashScript(tx.to); | |
transaction.txOuts[0] = { | |
value: BigInt(tx.amount), | |
pkScriptSize: pkScript.length, | |
pkScript, | |
}; | |
if (balance > tx.amount) { | |
// eslint-disable-next-line @typescript-eslint/no-shadow | |
const pkScript = serializePayToPubkeyHashScript(singleAddress); | |
const value = balance - tx.amount - BigInt(fee.amount); | |
if (value > 10000) { | |
transaction.txOuts[1] = { | |
value: BigInt(value.toString()), | |
pkScriptSize: pkScript.length, | |
pkScript, | |
}; | |
} | |
} | |
return { | |
data: transaction, | |
}; | |
} | |
async estimateTransactionCost(network: LitecoinNetwork, _wallet: WalletData, _tx: PreparedTransaction<SendRequest>, fee: FeeOption): Promise<TotalFee> { | |
if (!('amount' in fee)) { | |
throw new Error('called with wrong fee type'); | |
} | |
return { | |
token: network.nativeTokenCaipId, | |
amount: fee.amount, | |
}; | |
} | |
async estimateDefaultTransactionCost(network: LitecoinNetwork): Promise<TotalFee> { | |
return { | |
token: network.nativeTokenCaipId, | |
amount: '100000', | |
}; | |
} | |
async fetchUtxoFromHarmony(address: string, network: string) { | |
const harmony = await getHarmony(); | |
const response = await harmony.GET('/v1/utxo', { | |
params: { query: { address, network } }, | |
}); | |
const ret: UTXOIn[] = []; | |
for (const utxo of response.content ?? []) { | |
ret.push({ | |
previousOutput: { | |
hash: Buffer.from(utxo.transactionId, 'hex').reverse().toString('hex'), | |
index: utxo.index, | |
}, | |
sequence: 4294967294, | |
script: Buffer.from(utxo.script, 'hex'), | |
}); | |
} | |
return ret; | |
} | |
} | |
export const litecoinNetwork = new LitecoinNetwork(); | |
export const litecoinTransport = new LitecoinTransport(); | |
async function signTransaction(transaction: LitecoinTransaction, key: BIP32Interface, index: number, hashCodeType: number) { | |
const rawUnsignedTransaction = prepareTransactionToSign(transaction, index, hashCodeType); | |
const rawTransactionHash = doubleHash(rawUnsignedTransaction); | |
let signature = secp256k1.ecdsaSign(rawTransactionHash, key.privateKey!); | |
signature = secp256k1.signatureExport(signature.signature); | |
return { signature: Buffer.from(signature), publicKey: key.publicKey }; | |
} | |
function addP2KHSignature(transaction: LitecoinTransaction, signature: Buffer, publicKey: Buffer, index: number) { | |
const signatureCompactSize = CompactSize.fromSize(signature.length + 1); | |
const publicKeyCompactSize = CompactSize.fromSize(publicKey.length); | |
const scriptSig = signatureCompactSize.toString('hex') + signature.toString('hex') + '01' + publicKeyCompactSize.toString('hex') + publicKey.toString('hex'); | |
transaction.txIns[index].signatureSize = CompactSize.fromSize(Buffer.from(scriptSig).length); | |
transaction.txIns[index].signature = Buffer.from(scriptSig, 'hex'); | |
return transaction; | |
} | |
function serializeTransaction(transaction: LitecoinTransaction) { | |
const txInCount = CompactSize.fromSize(transaction.txIns.length); | |
const txOutCount = CompactSize.fromSize(transaction.txOuts.length); | |
let bufferSize = 4 + txInCount.length; | |
for (let txIn = 0; txIn < transaction.txIns.length; txIn++) { | |
bufferSize += 32 + 4 + transaction.txIns[txIn].signatureSize!.length + transaction.txIns[txIn].signature!.length + 4; | |
} | |
bufferSize += txOutCount.length; | |
for (let txOut = 0; txOut < transaction.txOuts.length; txOut++) { | |
bufferSize += 8 + CompactSize.fromSize(transaction.txOuts[txOut].pkScriptSize).length + transaction.txOuts[txOut].pkScriptSize; | |
} | |
bufferSize += 4; | |
let buffer = Buffer.alloc(bufferSize); | |
let offset = 0; | |
buffer.writeUInt32LE(transaction.version, offset); | |
offset += 4; | |
txInCount.copy(buffer, offset); | |
offset += txInCount.length; | |
for (let txInIndex = 0; txInIndex < transaction.txIns.length; txInIndex++) { | |
Buffer.from(transaction.txIns[txInIndex].previousOutput.hash, 'hex').copy(buffer, offset); | |
offset += 32; | |
buffer.writeUInt32LE(transaction.txIns[txInIndex].previousOutput.index, offset); | |
offset += 4; | |
const scriptSigSize = CompactSize.fromSize(transaction.txIns[txInIndex].signature!.length); | |
scriptSigSize.copy(buffer, offset); | |
offset += scriptSigSize.length; | |
transaction.txIns[txInIndex].signature!.copy(buffer, offset); | |
offset += transaction.txIns[txInIndex].signature!.length; | |
buffer.writeUInt32LE(transaction.txIns[txInIndex].sequence, offset); | |
offset += 4; | |
} | |
txOutCount.copy(buffer, offset); | |
offset += txOutCount.length; | |
for (let txOutIndex = 0; txOutIndex < transaction.txOuts.length; txOutIndex++) { | |
let before = buffer.toString('hex'); | |
let value2write = new BigNumber(transaction.txOuts[txOutIndex].value.toString()).toString(16); | |
if (value2write.length % 2 !== 0) { | |
value2write = '0' + value2write; | |
} | |
value2write = Buffer.from(value2write, 'hex').reverse().toString('hex'); | |
for (let cc = 0; cc < value2write.length; cc++) { | |
before = setCharAt(before, cc + offset * 2, value2write[cc]); | |
} | |
buffer = Buffer.from(before, 'hex'); | |
offset += 8; | |
const pkScriptSize = CompactSize.fromSize(transaction.txOuts[txOutIndex].pkScriptSize); | |
pkScriptSize.copy(buffer, offset); | |
offset += pkScriptSize.length; | |
transaction.txOuts[txOutIndex].pkScript.copy(buffer, offset); | |
offset += transaction.txOuts[txOutIndex].pkScriptSize; | |
} | |
buffer.writeUInt32LE(transaction.locktime, offset); | |
offset += 4; | |
return buffer; | |
} | |
function prepareTransactionToSign(transaction: LitecoinTransaction, vint: number, hashCodeType: number) { | |
const txInCount = CompactSize.fromSize(transaction.txIns.length); | |
const txOutCount = CompactSize.fromSize(transaction.txOuts.length); | |
let bufSize = 4 + 1; | |
bufSize += 41 * transaction.txIns.length + transaction.txIns[vint].script.length; | |
bufSize += 1; | |
for (const txout of transaction.txOuts) { | |
bufSize += 9 + txout.pkScriptSize; | |
} | |
bufSize += 8; | |
let buffer = Buffer.alloc(bufSize); | |
let offset = 0; | |
buffer.writeUInt32LE(transaction.version, offset); | |
offset += 4; | |
txInCount.copy(buffer, offset); | |
offset += txInCount.length; | |
for (let txInIndex = 0; txInIndex < transaction.txIns.length; txInIndex++) { | |
Buffer.from(transaction.txIns[txInIndex].previousOutput.hash, 'hex').copy(buffer, offset); | |
offset += 32; | |
buffer.writeUInt32LE(transaction.txIns[txInIndex].previousOutput.index, offset); | |
offset += 4; | |
if (txInIndex === vint) { | |
const scriptSigSize = CompactSize.fromSize(transaction.txIns[txInIndex].script.length); | |
scriptSigSize.copy(buffer, offset); | |
offset += scriptSigSize.length; | |
transaction.txIns[txInIndex].script.copy(buffer, offset); | |
offset += transaction.txIns[txInIndex].script.length; | |
} else { | |
const nullBuffer = Buffer.alloc(1); | |
nullBuffer.copy(buffer, offset); | |
offset += nullBuffer.length; | |
} | |
buffer.writeUInt32LE(transaction.txIns[txInIndex].sequence, offset); | |
offset += 4; | |
} | |
txOutCount.copy(buffer, offset); | |
offset += txOutCount.length; | |
for (const txOutIndex in transaction.txOuts) { | |
let before = buffer.toString('hex'); | |
let value2write = new BigNumber(transaction.txOuts[txOutIndex].value.toString()).toString(16); | |
if (value2write.length % 2 !== 0) { | |
value2write = '0' + value2write; | |
} | |
value2write = Buffer.from(value2write, 'hex').reverse().toString('hex'); | |
for (let cc = 0; cc < value2write.length; cc++) { | |
before = setCharAt(before, cc + offset * 2, value2write[cc]); | |
} | |
buffer = Buffer.from(before, 'hex'); | |
offset += 8; | |
const pkScriptSize = CompactSize.fromSize(transaction.txOuts[txOutIndex].pkScriptSize); | |
pkScriptSize.copy(buffer, offset); | |
offset += pkScriptSize.length; | |
transaction.txOuts[txOutIndex].pkScript.copy(buffer, offset); | |
offset += transaction.txOuts[txOutIndex].pkScriptSize; | |
} | |
buffer.writeUInt32LE(transaction.locktime, offset); | |
offset += 4; | |
buffer.writeUInt32LE(hashCodeType, offset); | |
return buffer; | |
} | |
function setCharAt(str: string, index: number, chr: string) { | |
if (index > str.length - 1) { | |
return str; | |
} | |
return str.substring(0, index) + chr + str.substring(index + 1); | |
} | |
function doubleHash(data: Buffer) { | |
let hash = crypto.createHash('sha256').update(data).digest(); | |
hash = crypto.createHash('sha256').update(hash).digest(); | |
return hash; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment