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
38 changes: 35 additions & 3 deletions packages/cashscript/src/Instance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { BITBOX } from 'bitbox-sdk';
import { AddressDetailsResult } from 'bitcoin-com-rest';
import {
BITBOX,
} from 'bitbox-sdk';
import {
AddressDetailsResult,
AddressUtxoResult,
AddressUnconfirmedResult,
} from 'bitcoin-com-rest';
import { Artifact, Script, AbiFunction } from 'cashc';
import {
Utxo,
} from './interfaces';
import {
bitbox,
AddressUtil,
Expand All @@ -10,7 +19,10 @@ import {
import { Transaction } from './Transaction';
import { ContractFunction } from './Contract';
import { Parameter, encodeParameter } from './Parameter';
import { countOpcodes, calculateBytesize } from './util';
import {
countOpcodes,
calculateBytesize,
} from './util';

export class Instance {
name: string;
Expand All @@ -29,6 +41,26 @@ export class Instance {
return details.balanceSat + details.unconfirmedBalanceSat;
}

private async getUnconfirmed(): Promise<Utxo[]> {
const { utxos } = await this.bitbox.Address
.unconfirmed(this.address) as AddressUnconfirmedResult;
return utxos;
}

private async getUtxo(): Promise<Utxo[]> {
const { utxos } = await this.bitbox.Address.utxo(this.address) as AddressUtxoResult;
return utxos;
}

async getUtxos(excludeUnconfirmed?: boolean): Promise<Utxo[]> {
const promises = [this.getUtxo()];
if (!excludeUnconfirmed) {
promises.push(this.getUnconfirmed());
}
const results = await Promise.all(promises);
return results.reduce((memo, utxos) => memo.concat(utxos), []);
}

constructor(
artifact: Artifact,
private redeemScript: Script,
Expand Down
15 changes: 12 additions & 3 deletions packages/cashscript/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class Transaction {
private async createTransaction(
outs: Output[],
options?: TxOptions,
): Promise<{ tx: any, inputs: Utxo[]}> {
): Promise<{ tx: any, inputs: Utxo[] }> {
const sequence = options?.age
? this.builder.bip68.encode({ blocks: options.age })
: 0xfffffffe;
Expand All @@ -115,7 +115,12 @@ export class Transaction {

this.builder.setLockTime(locktime);

const { inputs, outputs } = await this.getInputsAndOutputs(outs, options?.fee);
const { inputs, outputs } = await this.getInputsAndOutputs(
outs,
options?.fee,
options?.minChange,
options?.inputs,
);

inputs.forEach((utxo) => {
this.builder.addInput(utxo.txid, utxo.vout, sequence);
Expand Down Expand Up @@ -168,9 +173,13 @@ export class Transaction {
outs: Output[],
hardcodedFee?: number,
minChange: number = DUST_LIMIT,
hardcodedUtxos?: Utxo[],
satsPerByte: number = 1.0,
): Promise<{ inputs: Utxo[], outputs: OutputForBuilder[] }> {
const { utxos } = await this.bitbox.Address.utxo(this.address) as AddressUtxoResult;
let utxos = hardcodedUtxos;
if (!utxos) {
({ utxos } = await this.bitbox.Address.utxo(this.address) as AddressUtxoResult);
}

// Use a placeholder script with 65-length Buffer in the place of signatures
// and a correctly sized preimage Buffer if the function is a covenant
Expand Down
37 changes: 36 additions & 1 deletion packages/cashscript/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ export interface Utxo {
vout: number;
amount: number;
satoshis: number;
height: number;
confirmations: number;
height?: number;
ts?: number;
}

export type Output = Recipient | OpReturn;
Expand All @@ -26,11 +27,45 @@ export interface OpReturn {
opReturn: string[];
}

export interface ScriptSigDetails {
asm: string;
hex: string;
}

export interface TxnDetailValueIn {
cashAddress: string;
legacyAddress: string;
n: number;
scriptSig: ScriptSigDetails;
sequence: number;
txid: string;
value: number;
vout: number;
}

export interface ScriptPubKeyDetails {
addresses: string[];
cashAddrs: string[];
asm: string;
hex: string;
type: string;
}

export interface TxnDetailValueOut {
n: number;
scriptPubKey: ScriptPubKeyDetails;
spendHeight: null | number;
spendIndex: null | number;
spendTxId: null | number;
value: string;
}

export interface TxOptions {
time?: number;
age?: number;
fee?: number;
minChange?: number;
inputs?: Utxo[];
}

export enum SignatureAlgorithm {
Expand Down
6 changes: 5 additions & 1 deletion packages/cashscript/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import {
Data,
Op,
} from 'cashc';
import { Utxo, OpReturn, OutputForBuilder } from './interfaces';
import {
Utxo,
OpReturn,
OutputForBuilder,
} from './interfaces';
import { ScriptUtil, CryptoUtil } from './BITBOX';
import { P2PKH_OUTPUT_SIZE, VERSION_SIZE, LOCKTIME_SIZE } from './constants';
import {
Expand Down
75 changes: 74 additions & 1 deletion packages/cashscript/test/e2e/P2PKH.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
bob,
} from '../fixture/vars';
import { getTxOutputs } from '../test-util';
import { isOpReturn } from '../../src/interfaces';
import {
isOpReturn,
Utxo,
TxnDetailValueIn,
} from '../../src/interfaces';
import { createOpReturnOutput } from '../../src/util';
import { FailedSigCheckError, Reason } from '../../src/Errors';

Expand Down Expand Up @@ -50,6 +54,53 @@ describe('P2PKH', () => {
const txOutputs = getTxOutputs(tx);
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }]));
});

it('should fail when not enough satoshis are provided in utxos', async () => {
// when
const to = p2pkhInstance.address;
const amount = 1000;
const utxos = await p2pkhInstance.getUtxos();
utxos.sort((a, b) => (a.satoshis > b.satoshis ? 1 : -1));
const { utxos: gathered, failureAmount } = gatherUtxos(utxos, { amount });

// send
await expect(
p2pkhInstance.functions
.spend(alicePk, new Sig(alice))
.send(to, failureAmount, { inputs: gathered }),
).rejects.toThrow();

await expect(
p2pkhInstance.functions
.spend(alicePk, new Sig(alice))
.send(to, failureAmount),
).resolves.toBeTruthy();
});

it('should succeed when defining its own utxos', async () => {
expect.hasAssertions();

// given
const to = p2pkhInstance.address;
const amount = 1000;
const utxos = await p2pkhInstance.getUtxos();
utxos.sort((a, b) => (a.satoshis > b.satoshis ? 1 : -1));
const { utxos: gathered } = gatherUtxos(utxos, { amount });

// when
const receipt = await p2pkhInstance.functions
.spend(alicePk, new Sig(alice))
.send(to, amount, { inputs: gathered });

// then
for (const input of receipt.vin as TxnDetailValueIn[]) {
expect(gathered.find(utxo => (
utxo.txid === input.txid
&& utxo.vout === input.vout
&& utxo.satoshis === input.value
))).toBeTruthy();
}
});
});

describe('send (to many)', () => {
Expand Down Expand Up @@ -142,3 +193,25 @@ describe('P2PKH', () => {
});
});
});

function gatherUtxos(utxos: Utxo[], options?: {
amount?: number,
fees?: number
}): { utxos: Utxo[], total: number, failureAmount: number } {
const targetUtxos: Utxo[] = [];
let total = 0;
let failureAmount = 0;
// 1000 for fees
const { amount = 0, fees = 1000 } = options || {};
for (const utxo of utxos) {
failureAmount += utxo.satoshis;
if (total - fees > amount) break;
total += utxo.satoshis;
targetUtxos.push(utxo);
}
return {
utxos: targetUtxos,
total,
failureAmount,
};
}