BITBOX Integration¶
Spedn is available for NodeJS developers as an SDK extending capabilities of BITBOX SDK. TypeScript type definitions are provided out of the box.
Installation¶
NodeJS v11 or newer is required. You can also use v10 but then Worker Threads feature
has to be explicitly enabled by --experimental-worker
flag.
To install Spedn SDK in your JS project, type:
npm i spedn
# or
yarn add spedn
Compiler service¶
Spedn compiler runs as a service in a worker thread that you can start, use and dispose with Spedn
class.
import { Spedn } from "spedn";
async function main() {
const compiler = new Spedn();
/* use compiler */
compiler.dispose();
}
main();
Instead of manually disposing the service you can also use using
function inspired by some languages,
which guarantees automatic disposal of a resource also in case of exceptions.
import { Spedn, using } from "spedn";
async main() {
await using(new Spedn(), async compiler => {
/* use compiler */
});
}
main();
Compiling contracts¶
To compile a source file use compileFile
method.
To compile source code in a string, use compileCode
.
const { BlindEscrow } = await compiler.compileFile("./BlindEscrow.spedn");
const { ExpiringTip } = await compiler.compileCode(::`
contract ExpiringTip(Ripemd160 alice, Ripemd160 bob) {
challenge receive(Sig sig, PubKey pubKey) {
verify hash160(pubKey) == bob;
verify checkSig(sig, pubKey);
}
challenge revoke(Sig sig, PubKey pubKey) {
verify checkSequence(7d);
verify hash160(pubKey) == alice;
verify checkSig(sig, pubKey);
}
}
`);
The output of those methods is a JavaScript class representing a contract template.
Static field params
describes what parameters are required to instantiate it.
console.log(ExpiringTip.params);
// Object {alice: "Ripemd160", bob: "Ripemd160"}
Instantiating contracts¶
To instantiate the template, just create an object of the contract class, providing parameters values.
Parameters are passed as an object literal explicitly assigning values by names. Values of bool
and int
Spedn type can be passed as ordinary JS booleans and numbers. Time
and TimeSpan
are also passed as numbers
(see BIP65 and BIP112 for value interpretation details).
All the other types should be passed as JS Buffer
.
In case of ExpiringTip
you’ll need 2 public keys which you can generate with BITBOX.
import { BITBOX } from "bitbox-sdk";
const bitbox = new BITBOX();
const mnemonic = "draw parade crater busy book swim soldier tragic exit feel top civil";
const wallet = bitbox.HDNode.fromSeed(bitbox.Mnemonic.toSeed(mnemonic));
const alice = bitbox.HDNode.derivePath(wallet, "m/44'/145'/0'/0/0");
const bob = bitbox.HDNode.derivePath(wallet, "m/44'/145'/1'/0/0");
const tip = new ExpiringTip({
alice: alice.getIdentifier(), // Ripemd160 hash of Alice's public key
bob: bob.getIdentifier() // Ripemd160 hash of Bob's public key
});
Once created, you can read the contract funding address and lookup for UTXOs (coins) that are locked in it.
Also, a field challengeSpecs
contains definitions of challenges and their parameters.
console.log(tip.getAddress("mainnet"));
// bitcoincash:pppvx30pcylxzhewr6puknpuvz7gjjtl4sdw4ezcnp
const coins = await tip.findCoins("mainnet");
// Array(2) [.....]
console.log(tip.challengeSpecs);
// Object {receive: Object, revoke: Object}
console.log(tip.challengeSpecs.receive);
// Object {sig: "Sig", pubKey: "PubKey"}
Spending coins¶
To spend coins, use TxBuilder
. Provide tx inputs with from
method and outputs with to
method.
Optionally, set a timelock with withTimelock
.
To send the transaction to the network use broadcast
method.
If you just want to build the transaction without broadcasting it, use build
method.
from
method accept a single coin or an array of coins as a first parameter.
Because you can’t (in most cases) sign the input without defining all the inputs and outputs first,
from
method does not simply accept scriptSig parameter. Instead, it accepts a SigningCallback
function
and the actual signing is deferred to the moment of calling build
/broadcast
.
SigningCallback
accepts 2 parameters. The first one is an object containing contract challenges.
The second one is a SigningContext
which provides methods necessary for signing:
sign(keyPair, hashType)
- generates a siggnature valid forOP_CHECKSIG
.signData(keyPair, data)
- generates a signature valid forOP_CHECKDATASIG
.preimage(hashType)
- generates the same preimage as one used bysign(keyPair, hashType)
(useful forOP_CHECKDATASIG
covenants).
Note that methods accepting hashType
always add SIGHASH_FORKID
flag so you don’t need to specify it
explicitly.
to
method accepts an address or a scriptPubKey buffer as its first argument and an amount (in satoshis)
as the second one. You can also omit the amount at a single output - in this case, TxBuilder
will
treat this output as a change address and automatically calculate its amount choosing optimal transaction fee.
In the following example, all the previously found coins are spent using receive
challenge but 5mBCH goes to
Bob’s new address and the rest goes back to Alice.
import { TxBuilder, SigHash } from "spedn";
const txid = await new TxBuilder("mainnet")
.from(coins, (input, context) =>
input.receive({
sig: context.sign(bob.keyPair, SigHash.SIGHASH_ALL),
pubKey: bob.getPublicKeyBuffer()
})
)
.to("bitcoincash:qrc2jhalczuka8q3dvk0g8mnkqx79wxp9gvvqvg7qt", 500000)
.to(alice.getAddress())
.withTimelock(567654)
.broadcast();
Spending ordinary P2PKH¶
Spedn SDK provides also a class P2PKH
which is a representation of an ordinary Pay to Public Key Hash address.
You can instantiate it with a public key hash buffer or several factory methods:
import { P2PKH } from "spedn";
let addr = new P2PKH(bob.getIdentifier());
addr = P2PKH.fromKeyPair(bob.keyPair);
addr = P2PKH.fromPubKey(bob.getPublicKeyBuffer());
addr = P2PKH.fromAddress(bob.getAddress());
// all the above are equivalent
P2PKH contracts can be spent just like any other contract - they have spend({sig, pubKey})
challenge,
but you can also replace the whole signing callback with a convenient helper signWith(keyPair)
.
Let’s modify the previous example to spend additional input.
import { signWith } from "spedn";
const bobsCoins = await addr.findCoins("mainnet");
const txid = await new TxBuilder("mainnet")
.from(coins, (input, context) =>
input.receive({
sig: context.sign(bob.keyPair, SigHash.SIGHASH_ALL),
pubKey: bob.getPublicKeyBuffer()
})
)
.from(bobsCoins[14], signWith(bob.keyPair))
.to("bitcoincash:qrc2jhalczuka8q3dvk0g8mnkqx79wxp9gvvqvg7qt", 500000)
.to(alice.getAddress())
.withTimelock(567654)
.broadcast();
Spending generic P2SH¶
Spedn SDK provides also a class GenericP2SH
for interoperability with any Pay to Script Hash contract
created without Spedn. To work with that kind of contract, you just need to know its redeemScript and
what arguments it expects. The generated class will have a single challenge spend
with parameter
requirements as specified in the constructor.
import { GenericP2SH } from "spedn";
const contract = new GenericP2SH(redeemScriptBuffer, { sig: "Sig", someNumber: "int" });