Skip to content

Instantly share code, notes, and snippets.

@losh11
Created April 18, 2024 22:20
Show Gist options
  • Save losh11/fede191c14a654338771a66b13902363 to your computer and use it in GitHub Desktop.
Save losh11/fede191c14a654338771a66b13902363 to your computer and use it in GitHub Desktop.
/* 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