Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions packages/cashscript/src/TransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import {
StandardUnlockableUtxo,
VmResourceUsage,
isContractUnlocker,
isPlaceholderUnlocker,
BchChangeOutputOptions,
TokenChangeOutputOptions,
} from './interfaces.js';
import { PLACEHOLDER_P2PKH_UNLOCKING_SIZE } from './constants.js';
import { NetworkProvider } from './network/index.js';
import {
calculateDust,
Expand Down Expand Up @@ -278,12 +280,19 @@ export class TransactionBuilder {

/**
* Build the transaction (skipping fee and burn checks) and return its encoded byte length.
* Inputs with a placeholder unlocker are counted at the size they take up once the wallet signs them.
*
* @returns The size of the transaction in bytes.
*/
getTransactionSize(): bigint {
const transaction = this.buildLibauthTransaction(true);
return BigInt(encodeTransaction(transaction).byteLength);
return BigInt(this.getEncodedTransactionSize(transaction));
}

// Placeholder unlockers serialise as an empty unlocking script, so their eventual signed size is added here
private getEncodedTransactionSize(transaction: LibauthTransaction): number {
const placeholderInputCount = this.inputs.filter((input) => isPlaceholderUnlocker(input.unlocker)).length;
return encodeTransaction(transaction).byteLength + placeholderInputCount * PLACEHOLDER_P2PKH_UNLOCKING_SIZE;
}
Comment on lines 281 to 296

/**
Expand Down Expand Up @@ -561,7 +570,7 @@ export class TransactionBuilder {
private checkFee(transaction: LibauthTransaction): void {
const totalInputAmount = this.inputs.reduce((total, input) => total + input.satoshis, 0n);
const totalOutputAmount = this.outputs.reduce((total, output) => total + output.amount, 0n);
const transactionSize = encodeTransaction(transaction).byteLength;
const transactionSize = this.getEncodedTransactionSize(transaction);

const fee = totalInputAmount - totalOutputAmount;
const feePerByte = Number((Number(fee) / transactionSize).toFixed(2));
Expand Down Expand Up @@ -623,7 +632,7 @@ export class TransactionBuilder {
}

private checkTransactionSize(transaction: LibauthTransaction): void {
const transactionSize = encodeTransaction(transaction).byteLength;
const transactionSize = this.getEncodedTransactionSize(transaction);

const TX_MAX_STANDARD_SIZE = 100_000;
if (transactionSize > TX_MAX_STANDARD_SIZE) {
Expand Down
3 changes: 2 additions & 1 deletion packages/cashscript/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const VERSION_SIZE = 4;
export const LOCKTIME_SIZE = 4;
export const P2PKH_INPUT_SIZE = 32 + 4 + 1 + 1 + 65 + 1 + 33 + 4;
// Size of a placeholder P2PKH unlocking script using Schnorr signatures: push(signature) push(pk).
export const PLACEHOLDER_P2PKH_UNLOCKING_SIZE = 1 + 65 + 1 + 33;
Comment on lines +3 to +4
35 changes: 35 additions & 0 deletions packages/cashscript/test/TransactionBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,41 @@ describe('Transaction Builder', () => {
});
});

describe('test TransactionBuilder.getTransactionSize', () => {
it('should size placeholder P2PKH inputs as signed inputs', async () => {
const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo);

const placeholderSize = new TransactionBuilder({ provider })
.addInput(aliceUtxos[0], placeholderP2PKHUnlocker(aliceAddress))
.addOutput({ to: aliceAddress, amount: 1000n })
.getTransactionSize();

const signedSize = new TransactionBuilder({ provider })
.addInput(aliceUtxos[0], new SignatureTemplate(alicePriv).unlockP2PKH())
.addOutput({ to: aliceAddress, amount: 1000n })
.getTransactionSize();

// the wallet signs with a 65-byte Schnorr signature, so the placeholder is sized exactly like the signed input
expect(placeholderSize).toBe(signedSize);
});
Comment on lines +267 to +269

it('should pay exactly the fee rate once a placeholder input is signed', async () => {
const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo);

const builder = new TransactionBuilder({ provider })
.addInput(aliceUtxos[0], placeholderP2PKHUnlocker(aliceAddress))
.addOutput({ to: aliceAddress, amount: 1000n })
.addBchChangeOutputIfNeeded({ to: aliceAddress, feeRate: 1 });

const signedSize = new TransactionBuilder({ provider })
.addInput(aliceUtxos[0], new SignatureTemplate(alicePriv).unlockP2PKH())
.addOutputs(builder.outputs)
.getTransactionSize();

expect(builder.calculateTransactionFee().feeSats).toBe(signedSize);
});
});

it('should not fail when validly spending from only P2PKH inputs', async () => {
const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo);
const sigTemplate = new SignatureTemplate(alicePriv);
Expand Down
Loading