Skip to content

Commit

Permalink
bch
Browse files Browse the repository at this point in the history
  • Loading branch information
TheTrunk committed Jun 8, 2024
1 parent 3ba0078 commit d051ec0
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 251 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@scure/bip39": "~1.3.0",
"assert": "~2.1.0",
"axios": "~1.7.2",
"bchaddrjs": "~0.5.2",
"bignumber.js": "~9.1.2",
"buffer": "~6.0.3",
"crypto-js": "~4.2.0",
Expand Down Expand Up @@ -66,6 +67,7 @@
"util": "~0.12.5"
},
"devDependencies": {
"@types/bchaddrjs": "~0.4.3",
"@babel/core": "~7.24.6",
"@babel/preset-env": "~7.24.6",
"@babel/runtime": "~7.24.6",
Expand Down
15 changes: 15 additions & 0 deletions src/assets/bch.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/blockchains.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ declare module '@storage/blockchains' {
txGroupID: number;
backend: string;
bech32: string;
dustLimit: number;
minFeePerByte: number;
feePerByte: number;
maxMessage: number;
maxTxSize: number;
rbf: boolean;
cashaddr: string;
}
type blockchains = Record<string, Blockchain>;
let blockchains: blockchains;
Expand Down
235 changes: 0 additions & 235 deletions src/lib/constructTx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,241 +214,6 @@ export function signTransaction(
}
}

// entire utxo set will be used to construct the tx, amount, fee is in satoshi represented as string
export function buildUnsignedRawTx(
chain: keyof cryptos,
utxos: utxo[],
receiver: string,
amount: string,
fee: string,
change: string,
message: string,
): string {
try {
const libID = getLibId(chain);
const network = utxolib.networks[libID];
const txb = new utxolib.TransactionBuilder(network, fee);
if (blockchains[chain].txVersion) {
txb.setVersion(blockchains[chain].txVersion);
}
if (blockchains[chain].txGroupID) {
txb.setVersionGroupId(blockchains[chain].txGroupID);
}
utxos.forEach((x) => txb.addInput(x.txid, x.vout));
const recipients = [
{
address: receiver,
satoshis: amount,
},
];
let totalUtxoValue = new BigNumber(0);
utxos.forEach((x) => {
totalUtxoValue = totalUtxoValue.plus(new BigNumber(x.satoshis));
});

// if fee + amount is bigger than all our utxo satoshi combined, add our change address output
const amountToSend = new BigNumber(amount);
const feeToSend = new BigNumber(fee);
const totalAmountOutgoing = amountToSend.plus(feeToSend);
if (totalUtxoValue.isGreaterThan(totalAmountOutgoing)) {
// we do have a change, add it to the recipients
recipients.push({
address: change,
satoshis: totalUtxoValue.minus(totalAmountOutgoing).toFixed(),
});
}

// library accepts it as integer. BN is capped with max safe integer, throws otherwise
recipients.forEach((x) =>
txb.addOutput(x.address, new BigNumber(x.satoshis).toNumber()),
);

if (message) {
const data = Buffer.from(message, 'utf8');
const dataScript = utxolib.script.nullData.output.encode(data);
txb.addOutput(dataScript, 0);
}

const tx = txb.buildIncomplete();
const txhex = tx.toHex();
return txhex;
} catch (e) {
console.log(e);
throw e;
}
}

function pickUtxos(utxos: utxo[], amount: BigNumber): utxo[] {
let selectedUtxos: utxo[] = [];
// sorted utxos by satoshis, smallest first
const sortedUtxos = utxos.sort((a, b) => {
const aSatoshis = new BigNumber(a.satoshis);
const bSatoshis = new BigNumber(b.satoshis);
if (aSatoshis.isLessThan(bSatoshis)) {
return -1;
}
if (aSatoshis.isGreaterThan(bSatoshis)) {
return 1;
}
return 0;
});

// case one. Find if we have a utxo with exact amount
sortedUtxos.forEach((utxoX) => {
const utxoAmount = new BigNumber(utxoX.satoshis);
if (utxoAmount.isEqualTo(amount)) {
selectedUtxos = [utxoX];
}
});
if (selectedUtxos.length && selectedUtxos.length <= 670) {
return selectedUtxos;
}

// case two
// If the "sum of all your UTXO smaller than the Target" happens to match the Target, they will be used. (This is the case if you sweep a complete wallet.)
const utxosSmallerThanTarget = sortedUtxos.filter((utxoX) => {
const utxoAmount = new BigNumber(utxoX.satoshis);
return utxoAmount.isLessThan(amount);
});
let totalAmountSmallerUtxos = new BigNumber(0);
utxosSmallerThanTarget.forEach((utxoX) => {
const utxoAmount = new BigNumber(utxoX.satoshis);
totalAmountSmallerUtxos = totalAmountSmallerUtxos.plus(utxoAmount);
});
if (totalAmountSmallerUtxos.isEqualTo(amount)) {
selectedUtxos = utxosSmallerThanTarget;
}
if (selectedUtxos.length && selectedUtxos.length <= 670) {
return selectedUtxos;
}

// case three
// If the "sum of all your UTXO smaller than the Target" doesn't surpass the target, the smallest UTXO greater than your Target will be used.
const utxosBiggestThanTarget = sortedUtxos.filter((utxoX) => {
const utxoAmount = new BigNumber(utxoX.satoshis);
return utxoAmount.isGreaterThanOrEqualTo(amount);
});
if (totalAmountSmallerUtxos.isLessThan(amount)) {
if (utxosBiggestThanTarget.length) {
selectedUtxos = [utxosBiggestThanTarget[0]];
}
}
if (selectedUtxos.length && selectedUtxos.length <= 670) {
return selectedUtxos;
}

// case 4
// If the "sum of all your UTXO smaller than the Target" surpasses the Target, try using the smallest UTXO first and add more UTXO until you reach the Target.
if (totalAmountSmallerUtxos.isGreaterThanOrEqualTo(amount)) {
let totalAmount = new BigNumber(0);
const preselectedUtxos = [];
for (const utxoX of utxosSmallerThanTarget) {
totalAmount = totalAmount.plus(new BigNumber(utxoX.satoshis));
preselectedUtxos.push(utxoX);
if (totalAmount.isGreaterThanOrEqualTo(amount)) {
selectedUtxos = preselectedUtxos;
break;
}
}
if (selectedUtxos.length && selectedUtxos.length <= 670) {
return selectedUtxos;
}
}

// case 5
// If the "sum of all your UTXO smaller than the Target" surpasses the Target, try using the biggest UTXO first and add more UTXO until you reach the Target.
if (totalAmountSmallerUtxos.isGreaterThanOrEqualTo(amount)) {
let totalAmount = new BigNumber(0);
const preselectedUtxos = [];
for (const utxoX of utxosSmallerThanTarget.reverse()) {
totalAmount = totalAmount.plus(new BigNumber(utxoX.satoshis));
preselectedUtxos.push(utxoX);
if (totalAmount.isGreaterThanOrEqualTo(amount)) {
selectedUtxos = preselectedUtxos;
break;
}
}
if (selectedUtxos.length && selectedUtxos.length <= 670) {
return selectedUtxos;
}
}

// case 6, use utxo bigger than target
if (utxosBiggestThanTarget.length) {
selectedUtxos = [utxosBiggestThanTarget[0]];
}
if (selectedUtxos.length && selectedUtxos.length <= 670) {
return selectedUtxos;
}

// case 7, transaction can't be constructed, tx size would exceed 100kb. This is a limitation of the blockchain. Fallback to case 5
if (totalAmountSmallerUtxos.isGreaterThanOrEqualTo(amount)) {
let totalAmount = new BigNumber(0);
const preselectedUtxos = [];
for (const utxoX of utxosSmallerThanTarget.reverse()) {
totalAmount = totalAmount.plus(new BigNumber(utxoX.satoshis));
preselectedUtxos.push(utxoX);
if (totalAmount.isGreaterThanOrEqualTo(amount)) {
selectedUtxos = preselectedUtxos;
break;
}
}
}
if (selectedUtxos.length && selectedUtxos.length <= 670) {
return selectedUtxos;
}
throw new Error(
'Transaction can not be constructed. Try changing the amount.',
);
}

export async function constructAndSignTransaction(
chain: keyof cryptos,
receiver: string,
amount: string,
fee: string,
sender: string,
change: string,
message: string,
privateKey: string,
redeemScript: string,
witnessScript: string,
): Promise<string> {
try {
const utxos = await fetchUtxos(sender, chain);
const amountToSend = new BigNumber(amount).plus(new BigNumber(fee));
const pickedUtxos = pickUtxos(utxos, amountToSend);
const rawTx = buildUnsignedRawTx(
chain,
pickedUtxos,
receiver,
amount,
fee,
change,
message,
);
if (!rawTx) {
throw new Error('Could not construct raw tx');
}
const signedTx = signTransaction(
rawTx,
chain,
privateKey,
redeemScript,
witnessScript,
utxos,
);
if (!signedTx) {
throw new Error('Could not sign tx');
}
// wallet is NOT finalising the transaction, the KEY is finalising the transaction
return signedTx;
} catch (error) {
console.log(error);
throw error;
}
}

export async function broadcastTx(
txHex: string,
chain: keyof cryptos,
Expand Down
6 changes: 6 additions & 0 deletions src/lib/transactions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import BigNumber from 'bignumber.js';
import utxolib from '@runonflux/utxo-lib';
import { toCashAddress } from 'bchaddrjs';
import { cryptos, utxo } from '../types';

import { blockchains } from '@storage/blockchains';
Expand All @@ -20,6 +21,7 @@ export function decodeTransactionForApproval(
try {
const libID = getLibId(chain);
const decimals = blockchains[chain].decimals;
const cashAddrPrefix = blockchains[chain].cashaddr;
const network = utxolib.networks[libID];
const txhex = rawTx;
const txb = utxolib.TransactionBuilder.fromTransaction(
Expand Down Expand Up @@ -98,6 +100,10 @@ export function decodeTransactionForApproval(
// fee is negative, something is wrong. Reject.
throw new Error('Unexpected negative fee. Transaction Rejected.');
}
if (cashAddrPrefix) {
senderAddress = toCashAddress(senderAddress);
txReceiver = toCashAddress(txReceiver);
}
const txInfo = {
sender: senderAddress,
receiver: txReceiver,
Expand Down
18 changes: 15 additions & 3 deletions src/lib/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Buffer } from 'buffer';
import { HDKey } from '@scure/bip32';
import * as bip39 from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { toCashAddress } from 'bchaddrjs';
import { keyPair, minHDKey, multisig, xPrivXpub, cryptos } from '../types';
import { blockchains } from '@storage/blockchains';

Expand Down Expand Up @@ -104,6 +105,7 @@ export function generateMultisigAddress(
const bipParams = blockchains[chain].bip32;
const type = blockchains[chain].scriptType;
const networkBipParams = utxolib.networks[libID].bip32;
const cashAddrPrefix = blockchains[chain].cashaddr;
let externalChain1, externalChain2;
try {
externalChain1 = HDKey.fromExtendedKey(xpub1, bipParams);
Expand Down Expand Up @@ -141,7 +143,10 @@ export function generateMultisigAddress(
const scriptPubKey = utxolib.script.witnessScriptHash.output.encode(
utxolib.crypto.sha256(witnessScript),
);
const address = utxolib.address.fromOutputScript(scriptPubKey, network);
let address = utxolib.address.fromOutputScript(scriptPubKey, network);
if (cashAddrPrefix) {
address = toCashAddress(address);
}
const witnessScriptHex: string = Buffer.from(witnessScript).toString('hex');
return {
address,
Expand All @@ -158,9 +163,12 @@ export function generateMultisigAddress(
const scriptPubKey = utxolib.script.scriptHash.output.encode(
utxolib.crypto.hash160(redeemScript),
);
const address = utxolib.address.fromOutputScript(scriptPubKey, network);
let address = utxolib.address.fromOutputScript(scriptPubKey, network);
const witnessScriptHex: string = Buffer.from(witnessScript).toString('hex');
const redeemScriptHex: string = Buffer.from(redeemScript).toString('hex');
if (cashAddrPrefix) {
address = toCashAddress(address);
}
return {
address,
redeemScript: redeemScriptHex,
Expand All @@ -176,11 +184,15 @@ export function generateMultisigAddress(
utxolib.crypto.hash160(redeemScript),
);

const address: string = utxolib.address.fromOutputScript(
let address: string = utxolib.address.fromOutputScript(
scriptPubKey,
network,
);

if (cashAddrPrefix) {
address = toCashAddress(address);
}

const redeemScriptHex: string = Buffer.from(redeemScript).toString('hex');
return {
address,
Expand Down
4 changes: 4 additions & 0 deletions src/storage/backends.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const doge = {
const zec = {
node: 'blockbookzcash.app.runonflux.io',
};
const bch = {
node: 'blockbookbitcoincash.app.runonflux.io',
};
const btcTestnet = {
node: 'blockbookbitcointestnet.app.runonflux.io',
};
Expand All @@ -54,6 +57,7 @@ export function backends() {
btc: localForgeBackends?.btc || btc,
doge: localForgeBackends?.doge || doge,
zec: localForgeBackends?.zec || zec,
bch: localForgeBackends?.bch || bch,
btcTestnet: localForgeBackends?.btcTestnet || btcTestnet,
btcSignet: localForgeBackends?.btcSignet || btcSignet,
};
Expand Down
Loading

0 comments on commit d051ec0

Please sign in to comment.