Skip to content

Commit cc83e76

Browse files
authored
Add advanced TransactionBuilder (for combining contracts) (#161)
And bump version to v0.9.0
1 parent 559c9cd commit cc83e76

File tree

22 files changed

+821
-179
lines changed

22 files changed

+821
-179
lines changed

examples/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cashscript-examples",
33
"private": true,
4-
"version": "0.8.2",
4+
"version": "0.9.0",
55
"description": "Usage examples of the CashScript SDK",
66
"main": "p2pkh.js",
77
"type": "module",
@@ -11,8 +11,8 @@
1111
"@bitauth/libauth": "^2.0.0-alpha.8",
1212
"@types/node": "^12.7.8",
1313
"bip39": "^3.0.4",
14-
"cashc": "^0.8.2",
15-
"cashscript": "^0.8.2",
14+
"cashc": "^0.9.0",
15+
"cashscript": "^0.9.0",
1616
"typescript": "^4.9.5"
1717
}
1818
}

examples/pat_bug.ts

Lines changed: 0 additions & 110 deletions
This file was deleted.

packages/cashc/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cashc",
3-
"version": "0.8.2",
3+
"version": "0.9.0",
44
"description": "Compile Bitcoin Cash contracts to Bitcoin Cash Script or artifacts",
55
"keywords": [
66
"bitcoin",
@@ -50,7 +50,7 @@
5050
},
5151
"dependencies": {
5252
"@bitauth/libauth": "^2.0.0-alpha.8",
53-
"@cashscript/utils": "^0.8.2",
53+
"@cashscript/utils": "^0.9.0",
5454
"antlr4ts": "^0.5.0-alpha.4",
5555
"commander": "^7.1.0",
5656
"semver": "^7.3.4"

packages/cashc/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export * from './Errors.js';
22
export * as utils from '@cashscript/utils';
33
export { compileFile, compileString } from './compiler.js';
44

5-
export const version = '0.8.2';
5+
export const version = '0.9.0';

packages/cashscript/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cashscript",
3-
"version": "0.8.2",
3+
"version": "0.9.0",
44
"description": "Easily write and interact with Bitcoin Cash contracts",
55
"keywords": [
66
"bitcoin cash",
@@ -44,7 +44,7 @@
4444
},
4545
"dependencies": {
4646
"@bitauth/libauth": "^2.0.0-alpha.8",
47-
"@cashscript/utils": "^0.8.2",
47+
"@cashscript/utils": "^0.9.0",
4848
"bip68": "^1.0.4",
4949
"bitcoin-rpc-promise-retry": "^1.3.0",
5050
"delay": "^5.0.0",

packages/cashscript/src/Contract.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ import {
66
calculateBytesize,
77
countOpcodes,
88
generateRedeemScript,
9+
hash256,
910
Script,
1011
scriptToBytecode,
1112
} from '@cashscript/utils';
1213
import { Transaction } from './Transaction.js';
1314
import { Argument, encodeArgument } from './Argument.js';
14-
import { ContractOptions, Utxo } from './interfaces.js';
15+
import {
16+
Unlocker, ContractOptions, GenerateUnlockingBytecodeOptions, Utxo,
17+
} from './interfaces.js';
1518
import NetworkProvider from './network/NetworkProvider.js';
16-
import { scriptToAddress } from './utils.js';
19+
import {
20+
addressToLockScript, createInputScript, createSighashPreimage, scriptToAddress,
21+
} from './utils.js';
1722
import SignatureTemplate from './SignatureTemplate.js';
1823
import { ElectrumNetworkProvider } from './network/index.js';
1924

@@ -25,9 +30,8 @@ export class Contract {
2530
bytesize: number;
2631
opcount: number;
2732

28-
functions: {
29-
[name: string]: ContractFunction,
30-
};
33+
functions: Record<string, ContractFunction>;
34+
unlock: Record<string, ContractUnlocker>;
3135

3236
private redeemScript: Script;
3337
private provider: NetworkProvider;
@@ -38,10 +42,8 @@ export class Contract {
3842
constructorArgs: Argument[],
3943
private options?: ContractOptions,
4044
) {
41-
const defaultProvider = new ElectrumNetworkProvider();
42-
const defaultAddressType = 'p2sh32';
43-
this.provider = this.options?.provider ?? defaultProvider;
44-
this.addressType = this.options?.addressType ?? defaultAddressType;
45+
this.provider = this.options?.provider ?? new ElectrumNetworkProvider();
46+
this.addressType = this.options?.addressType ?? 'p2sh32';
4547

4648
const expectedProperties = ['abi', 'bytecode', 'constructorInputs', 'contractName'];
4749
if (!expectedProperties.every((property) => property in artifact)) {
@@ -79,6 +81,18 @@ export class Contract {
7981
});
8082
}
8183

84+
// Populate the functions object with the contract's functions
85+
// (with a special case for single function, which has no "function selector")
86+
this.unlock = {};
87+
if (artifact.abi.length === 1) {
88+
const f = artifact.abi[0];
89+
this.unlock[f.name] = this.createUnlocker(f);
90+
} else {
91+
artifact.abi.forEach((f, i) => {
92+
this.unlock[f.name] = this.createUnlocker(f, i);
93+
});
94+
}
95+
8296
this.name = artifact.contractName;
8397
this.address = scriptToAddress(this.redeemScript, this.provider.network, this.addressType, false);
8498
this.tokenAddress = scriptToAddress(this.redeemScript, this.provider.network, this.addressType, true);
@@ -116,6 +130,39 @@ export class Contract {
116130
);
117131
};
118132
}
133+
134+
private createUnlocker(abiFunction: AbiFunction, selector?: number): ContractUnlocker {
135+
return (...args: Argument[]) => {
136+
const bytecode = scriptToBytecode(this.redeemScript);
137+
138+
const encodedArgs = args
139+
.map((arg, i) => encodeArgument(arg, abiFunction.inputs[i].type));
140+
141+
const generateUnlockingBytecode = (
142+
{ transaction, sourceOutputs, inputIndex }: GenerateUnlockingBytecodeOptions,
143+
): Uint8Array => {
144+
const completeArgs = encodedArgs.map((arg) => {
145+
if (!(arg instanceof SignatureTemplate)) return arg;
146+
147+
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, bytecode, arg.getHashType());
148+
const sighash = hash256(preimage);
149+
150+
return arg.generateSignature(sighash);
151+
});
152+
153+
const unlockingBytecode = createInputScript(
154+
this.redeemScript, completeArgs, selector,
155+
);
156+
157+
return unlockingBytecode;
158+
};
159+
160+
const generateLockingBytecode = (): Uint8Array => addressToLockScript(this.address);
161+
162+
return { generateUnlockingBytecode, generateLockingBytecode };
163+
};
164+
}
119165
}
120166

121167
export type ContractFunction = (...args: Argument[]) => Transaction;
168+
export type ContractUnlocker = (...args: Argument[]) => Unlocker;

packages/cashscript/src/Errors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export class TokensToNonTokenAddressError extends Error {
1919
}
2020

2121
export class FailedTransactionError extends Error {
22-
constructor(public reason: string, public meep: string) {
23-
super(`Transaction failed with reason: ${reason}\n${meep}`);
22+
constructor(public reason: string, public meep?: string) {
23+
super(`Transaction failed with reason: ${reason}${meep ? `\n${meep}` : ''}`);
2424
}
2525
}
2626

packages/cashscript/src/SignatureTemplate.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { decodePrivateKeyWif, secp256k1, SigningSerializationFlag } from '@bitauth/libauth';
2-
import { HashType, SignatureAlgorithm } from './interfaces.js';
2+
import { hash256, scriptToBytecode } from '@cashscript/utils';
3+
import {
4+
Unlocker,
5+
GenerateUnlockingBytecodeOptions,
6+
HashType,
7+
SignatureAlgorithm,
8+
} from './interfaces.js';
9+
import { createSighashPreimage, publicKeyToP2PKHLockingBytecode } from './utils.js';
310

411
export default class SignatureTemplate {
512
private privateKey: Uint8Array;
@@ -34,6 +41,23 @@ export default class SignatureTemplate {
3441
getPublicKey(): Uint8Array {
3542
return secp256k1.derivePublicKeyCompressed(this.privateKey) as Uint8Array;
3643
}
44+
45+
unlockP2PKH(): Unlocker {
46+
const publicKey = this.getPublicKey();
47+
const prevOutScript = publicKeyToP2PKHLockingBytecode(publicKey);
48+
const hashtype = this.getHashType();
49+
50+
return {
51+
generateLockingBytecode: () => prevOutScript,
52+
generateUnlockingBytecode: ({ transaction, sourceOutputs, inputIndex }: GenerateUnlockingBytecodeOptions) => {
53+
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, prevOutScript, hashtype);
54+
const sighash = hash256(preimage);
55+
const signature = this.generateSignature(sighash);
56+
const unlockingBytecode = scriptToBytecode([signature, publicKey]);
57+
return unlockingBytecode;
58+
},
59+
};
60+
}
3761
}
3862

3963
// Works for both BITBOX/bitcoincash.js ECPair and bitcore-lib-cash PrivateKey

packages/cashscript/src/Transaction.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
Recipient,
2222
TokenDetails,
2323
NftObject,
24-
isSignableUtxo,
24+
isUtxoP2PKH,
2525
TransactionDetails,
2626
} from './interfaces.js';
2727
import {
@@ -169,7 +169,7 @@ export class Transaction {
169169
const sourceOutputs = this.inputs.map((input) => {
170170
const sourceOutput = {
171171
amount: input.satoshis,
172-
to: isSignableUtxo(input) ? publicKeyToP2PKHLockingBytecode(input.template.getPublicKey()) : lockingBytecode,
172+
to: isUtxoP2PKH(input) ? publicKeyToP2PKHLockingBytecode(input.template.getPublicKey()) : lockingBytecode,
173173
token: input.token,
174174
};
175175

@@ -189,7 +189,7 @@ export class Transaction {
189189

190190
this.inputs.forEach((utxo, i) => {
191191
// UTXO's with signature templates are signed using P2PKH
192-
if (isSignableUtxo(utxo)) {
192+
if (isUtxoP2PKH(utxo)) {
193193
const pubkey = utxo.template.getPublicKey();
194194
const prevOutScript = publicKeyToP2PKHLockingBytecode(pubkey);
195195

@@ -413,7 +413,7 @@ export class Transaction {
413413
// If inputs are already defined, the user provided the UTXOs and we perform no further UTXO selection
414414
if (!this.hardcodedFee) {
415415
const totalInputSize = this.inputs.reduce(
416-
(acc, input) => acc + (isSignableUtxo(input) ? P2PKH_INPUT_SIZE : contractInputSize),
416+
(acc, input) => acc + (isUtxoP2PKH(input) ? P2PKH_INPUT_SIZE : contractInputSize),
417417
0,
418418
);
419419
fee += addPrecision(totalInputSize * this.feePerByte);

0 commit comments

Comments
 (0)