Spaces:
Runtime error
Runtime error
Commit
Β·
711e9c6
1
Parent(s):
34e492e
Upload 34 files
Browse files- auction.md +296 -0
- bitcoin-basics.md +16 -0
- bsv.md +217 -0
- built-ins.md +716 -0
- call-deployed.md +122 -0
- deploy-cli.md +77 -0
- escrow.md +178 -0
- ethereum-devs.md +96 -0
- faq.md +41 -0
- faucet.md +30 -0
- hello-world.md +123 -0
- how-to-add-a-provider.md +301 -0
- how-to-add-a-signer.md +430 -0
- how-to-call-multiple-contracts.md +144 -0
- how-to-customize-a-contract-tx.md +118 -0
- how-to-debug-a-contract.md +75 -0
- how-to-debug-scriptcontext.md +39 -0
- how-to-deploy-and-call-a-contract.md +365 -0
- how-to-integrate-a-frontend.md +125 -0
- how-to-integrate-dotwallet.md +199 -0
- how-to-integrate-scrypt-service.md +157 -0
- how-to-publish-a-contract.md +161 -0
- how-to-test-a-contract.md +270 -0
- how-to-verify-a-contract.md +91 -0
- how-to-write-a-contract.md +658 -0
- inline-asm.md +59 -0
- installation.md +29 -0
- oracle.md +185 -0
- overview.md +53 -0
- scriptcontext.md +161 -0
- stateful-contract.md +108 -0
- tic-tac-toe.md +321 -0
- voting.md +706 -0
- zkp.md +305 -0
auction.md
ADDED
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 2
|
3 |
+
---
|
4 |
+
|
5 |
+
# Tutorial 2: Auction
|
6 |
+
|
7 |
+
## Overview
|
8 |
+
|
9 |
+
In this tutorial, we will go over how to build an auction contract. It is open and transparent, where everyone can participate and the highest bidder wins when the bidding is over.
|
10 |
+
|
11 |
+
There are two ways to interact with the contract:
|
12 |
+
|
13 |
+
1. Bid: if a higher bid is found, the current highest bidder is updated, and the previous highest bidder is refunded.
|
14 |
+
2. Close: the auctioneer can close the auction after it expires and take the offer.
|
15 |
+
|
16 |
+
## Contract Properties
|
17 |
+
|
18 |
+
According to the interactions above, this contract needs to store three properties:
|
19 |
+
|
20 |
+
- The auctioneer, who starts the auction
|
21 |
+
- The deadline for the auction
|
22 |
+
- The highest bidder until now
|
23 |
+
|
24 |
+
```ts
|
25 |
+
// The bidder's public key.
|
26 |
+
@prop(true)
|
27 |
+
bidder: PubKey
|
28 |
+
|
29 |
+
// The auctioneer's public key.
|
30 |
+
@prop()
|
31 |
+
readonly auctioneer: PubKey
|
32 |
+
|
33 |
+
// Deadline of the auction. Can be block height or timestamp.
|
34 |
+
@prop()
|
35 |
+
readonly auctionDeadline: bigint
|
36 |
+
```
|
37 |
+
|
38 |
+
## Constructor
|
39 |
+
|
40 |
+
Initialize all the `@prop` properties in the constructor. Note that we don't need to pass a `bidder` parameter.
|
41 |
+
|
42 |
+
```ts
|
43 |
+
constructor(auctioneer: PubKey, auctionDeadline: bigint) {
|
44 |
+
super(...arguments)
|
45 |
+
// the initial bidder is the auctioneer himeself
|
46 |
+
this.bidder = auctioneer
|
47 |
+
this.auctioneer = auctioneer
|
48 |
+
this.auctionDeadline = auctionDeadline
|
49 |
+
}
|
50 |
+
```
|
51 |
+
|
52 |
+
When deploying the contract, the auctioneer locked the minimal bid into the contract, and at this time, the highest bidder would be himself.
|
53 |
+
|
54 |
+
```ts
|
55 |
+
const auction = new Auction(publicKeyAuctioneer, auctionDeadline)
|
56 |
+
const deployTx = await auction.deploy(minBid)
|
57 |
+
```
|
58 |
+
|
59 |
+
## Public Methods
|
60 |
+
|
61 |
+
### Bid
|
62 |
+
|
63 |
+
In method `public bid(bidder: PubKeyHash, bid: bigint)`, we need to check if the bidder has a higher bid than the previous one. If so, we update the highest bidder in the contract state and refund the previous bidder.
|
64 |
+
|
65 |
+
We can read the previous highest bid from the balance of the contract UTXO.
|
66 |
+
|
67 |
+
```ts
|
68 |
+
const highestBid: bigint = this.ctx.utxo.value
|
69 |
+
```
|
70 |
+
|
71 |
+
Then it's easy to demand a higher bid.
|
72 |
+
|
73 |
+
```ts
|
74 |
+
assert(bid > highestBid, 'the auction bid is lower than the current highest bid')
|
75 |
+
```
|
76 |
+
|
77 |
+
The spending/redeeming tx has these outputs.
|
78 |
+
|
79 |
+

|
80 |
+
|
81 |
+
- Contract's new state output: records the new bidder and locks the new bid into contract UTXO.
|
82 |
+
|
83 |
+
```ts
|
84 |
+
// Log the previous highest bidder
|
85 |
+
const highestBidder: PubKey = this.bidder
|
86 |
+
// Change the public key of the highest bidder.
|
87 |
+
this.bidder = bidder
|
88 |
+
|
89 |
+
// Auction continues with a higher bidder.
|
90 |
+
const auctionOutput: ByteString = this.buildStateOutput(bid)
|
91 |
+
```
|
92 |
+
|
93 |
+
- A refund P2PKH output: pay back the previous bidder.
|
94 |
+
|
95 |
+
```ts
|
96 |
+
// Refund previous highest bidder.
|
97 |
+
const refundOutput: ByteString = Utils.buildPublicKeyHashOutput(highestBidder, highestBid)
|
98 |
+
```
|
99 |
+
|
100 |
+
- An optional change P2PKH output.
|
101 |
+
|
102 |
+
```ts
|
103 |
+
let outputs: ByteString = auctionOutput + refundOutput
|
104 |
+
// Add change output.
|
105 |
+
outputs += this.buildChangeOutput()
|
106 |
+
```
|
107 |
+
|
108 |
+
At last, we require the transaction to have these outputs using `ScriptContext`.
|
109 |
+
|
110 |
+
```ts
|
111 |
+
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs check failed')
|
112 |
+
```
|
113 |
+
|
114 |
+
As `bid` is called continuously, the state of the contract is constantly updated. The highest bidder, and the highest bid as well, is recorded in the latest contract UTXO until the auctioneer closes the auction.
|
115 |
+
|
116 |
+
```ts
|
117 |
+
// Call this public method to bid with a higher offer.
|
118 |
+
@method()
|
119 |
+
public bid(bidder: PubKeyHash, bid: bigint) {
|
120 |
+
const highestBid: bigint = this.ctx.utxo.value
|
121 |
+
assert(bid > highestBid, 'the auction bid is lower than the current highest bid')
|
122 |
+
|
123 |
+
// Change the public key of the highest bidder.
|
124 |
+
const highestBidder: PubKey = this.bidder
|
125 |
+
this.bidder = bidder
|
126 |
+
|
127 |
+
// Auction continues with a higher bidder.
|
128 |
+
const auctionOutput: ByteString = this.buildStateOutput(bid)
|
129 |
+
|
130 |
+
// Refund previous highest bidder.
|
131 |
+
const refundOutput: ByteString = Utils.buildPublicKeyHashOutput(highestBidder, highestBid)
|
132 |
+
|
133 |
+
let outputs: ByteString = auctionOutput + refundOutput
|
134 |
+
// Add change output.
|
135 |
+
outputs += this.buildChangeOutput()
|
136 |
+
|
137 |
+
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs check failed')
|
138 |
+
}
|
139 |
+
```
|
140 |
+
|
141 |
+
### Close
|
142 |
+
|
143 |
+

|
144 |
+
|
145 |
+
Method `public close(sig: Sig)` is simple, we require:
|
146 |
+
|
147 |
+
- It can only be called by the auctioneer. That is why we need to pass in the caller's signature.
|
148 |
+
|
149 |
+
```ts
|
150 |
+
// Check signature of the auctioneer.
|
151 |
+
assert(this.checkSig(sig, this.auctioneer), 'signature check failed')
|
152 |
+
```
|
153 |
+
|
154 |
+
- Now the auction deadline has passed
|
155 |
+
|
156 |
+
```ts
|
157 |
+
assert(this.ctx.locktime >= this.auctionDeadline, 'auction is not over yet')
|
158 |
+
```
|
159 |
+
|
160 |
+
:::note
|
161 |
+
We don't place any constraint on transaction outputs here, because the auctioneer can send the highest bid to any address he controls, which is what we actually want.
|
162 |
+
:::
|
163 |
+
|
164 |
+
```ts
|
165 |
+
// Close the auction if deadline is reached.
|
166 |
+
@method()
|
167 |
+
public close(sig: Sig) {
|
168 |
+
// Check deadline
|
169 |
+
assert(this.ctx.locktime >= this.auctionDeadline, 'auction is not over yet')
|
170 |
+
// Check signature of the auctioneer.
|
171 |
+
assert(this.checkSig(sig, this.auctioneer), 'signature check failed')
|
172 |
+
}
|
173 |
+
```
|
174 |
+
|
175 |
+
## Customize tx builder for `bid`
|
176 |
+
|
177 |
+
Using [default tx builder](../how-to-deploy-and-call-a-contract/how-to-customize-a-contract-tx.md#default-1) cannot meet our demand when calling `bid`, since the second output - the refund P2PKH output - is not a new contract instance.
|
178 |
+
|
179 |
+
In Function `static bidTxBuilder(options: MethodCallOptions<Auction>, bidder: PubKeyHash, bid: bigint): Promise<ContractTransaction>`, we add all three outputs as designed.
|
180 |
+
|
181 |
+
```ts
|
182 |
+
const unsignedTx: Transaction = new Transaction()
|
183 |
+
// add contract input
|
184 |
+
.addInput(current.buildContractInput(options.fromUTXO))
|
185 |
+
// build next instance output
|
186 |
+
.addOutput(new Transaction.Output({script: nextInstance.lockingScript, satoshis: Number(bid),}))
|
187 |
+
// build refund output
|
188 |
+
.addOutput(
|
189 |
+
new Transaction.Output({
|
190 |
+
script: Script.fromHex(Utils.buildPublicKeyHashScript(current.bidder)),
|
191 |
+
satoshis: options.fromUTXO?.satoshis ?? current.from.tx.outputs[current.from.outputIndex].satoshis,
|
192 |
+
})
|
193 |
+
)
|
194 |
+
// build change output
|
195 |
+
.change(options.changeAddress)
|
196 |
+
```
|
197 |
+
|
198 |
+
## Conclusion
|
199 |
+
|
200 |
+
Congratulations, you have completed the `Auction` contract!
|
201 |
+
|
202 |
+
The [final complete code](https://github.com/sCrypt-Inc/boilerplate/blob/master/src/contracts/auction.ts) is as follows:
|
203 |
+
|
204 |
+
```ts
|
205 |
+
export class Auction extends SmartContract {
|
206 |
+
static readonly LOCKTIME_BLOCK_HEIGHT_MARKER = 500000000
|
207 |
+
static readonly UINT_MAX = 0xffffffffn
|
208 |
+
|
209 |
+
// The bidder's public key.
|
210 |
+
@prop(true)
|
211 |
+
bidder: PubKey
|
212 |
+
|
213 |
+
// The auctioneer's public key.
|
214 |
+
@prop()
|
215 |
+
readonly auctioneer: PubKey
|
216 |
+
|
217 |
+
// Deadline of the auction. Can be block height or timestamp.
|
218 |
+
@prop()
|
219 |
+
readonly auctionDeadline: bigint
|
220 |
+
|
221 |
+
constructor(auctioneer: PubKey, auctionDeadline: bigint) {
|
222 |
+
super(...arguments)
|
223 |
+
this.bidder = auctioneer
|
224 |
+
this.auctioneer = auctioneer
|
225 |
+
this.auctionDeadline = auctionDeadline
|
226 |
+
}
|
227 |
+
|
228 |
+
// Call this public method to bid with a higher offer.
|
229 |
+
@method()
|
230 |
+
public bid(bidder: PubKey, bid: bigint) {
|
231 |
+
const highestBid: bigint = this.ctx.utxo.value
|
232 |
+
assert(bid > highestBid, 'the auction bid is lower than the current highest bid')
|
233 |
+
|
234 |
+
// Change the public key of the highest bidder.
|
235 |
+
const highestBidder: PubKey = this.bidder
|
236 |
+
this.bidder = bidder
|
237 |
+
|
238 |
+
// Auction continues with a higher bidder.
|
239 |
+
const auctionOutput: ByteString = this.buildStateOutput(bid)
|
240 |
+
|
241 |
+
// Refund previous highest bidder.
|
242 |
+
const refundOutput: ByteString = Utils.buildPublicKeyHashOutput(hash160(highestBidder), highestBid)
|
243 |
+
let outputs: ByteString = auctionOutput + refundOutput
|
244 |
+
|
245 |
+
// Add change output.
|
246 |
+
outputs += this.buildChangeOutput()
|
247 |
+
|
248 |
+
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs check failed')
|
249 |
+
}
|
250 |
+
|
251 |
+
// Close the auction if deadline is reached.
|
252 |
+
@method()
|
253 |
+
public close(sig: Sig) {
|
254 |
+
// Check deadline
|
255 |
+
assert(this.ctx.locktime >= this.auctionDeadline, 'auction is not over yet')
|
256 |
+
|
257 |
+
// Check signature of the auctioneer.
|
258 |
+
assert(this.checkSig(sig, this.auctioneer), 'signature check failed')
|
259 |
+
}
|
260 |
+
|
261 |
+
// User defined transaction builder for calling function `bid`
|
262 |
+
static bidTxBuilder(options: MethodCallOptions<Auction>, bidder: PubKey, bid: bigint): Promise<ContractTransaction> {
|
263 |
+
const current = options.current
|
264 |
+
|
265 |
+
const nextInstance = current.next()
|
266 |
+
nextInstance.bidder = bidder
|
267 |
+
|
268 |
+
const unsignedTx: Transaction = new Transaction()
|
269 |
+
// add contract input
|
270 |
+
.addInput(current.buildContractInput(options.fromUTXO))
|
271 |
+
// build next instance output
|
272 |
+
.addOutput(new Transaction.Output({script: nextInstance.lockingScript, satoshis: Number(bid),}))
|
273 |
+
// build refund output
|
274 |
+
.addOutput(
|
275 |
+
new Transaction.Output({
|
276 |
+
script: Script.fromHex(Utils.buildPublicKeyHashScript(hash160(current.bidder))),
|
277 |
+
satoshis: options.fromUTXO?.satoshis ?? current.from.tx.outputs[current.from.outputIndex].satoshis,
|
278 |
+
})
|
279 |
+
)
|
280 |
+
// build change output
|
281 |
+
.change(options.changeAddress)
|
282 |
+
|
283 |
+
return Promise.resolve({
|
284 |
+
tx: unsignedTx,
|
285 |
+
atInputIndex: 0,
|
286 |
+
nexts: [
|
287 |
+
{
|
288 |
+
instance: nextInstance,
|
289 |
+
atOutputIndex: 0,
|
290 |
+
balance: Number(bid),
|
291 |
+
},
|
292 |
+
],
|
293 |
+
})
|
294 |
+
}
|
295 |
+
}
|
296 |
+
```
|
bitcoin-basics.md
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 1
|
3 |
+
---
|
4 |
+
# Bitcoin Basics
|
5 |
+
|
6 |
+
If you are new to Bitcoin development, we recommend exploring the resources listed in this section. While not an absolute prerequisite, going over these guides will provide a clearer understanding of key concepts and make it easier to begin developing sCrypt smart contracts.
|
7 |
+
|
8 |
+
If you are already familiar with the basics of Bitcoin, you can skip ahead to the [How to Write a Contract section](../how-to-write-a-contract/how-to-write-a-contract.md).
|
9 |
+
|
10 |
+
## Useful Resources for Learning Bitcoin Fundamentals
|
11 |
+
|
12 |
+
If you want to learn more about the fundamentals of Bitcoin, consider exploring the following resources:
|
13 |
+
- [BSV Academy - Bitcoin Theory](https://bitcoinsv.academy/course/bitcoin-theory)
|
14 |
+
- [BSV Academy - Bitcoin Essentials](https://bitcoinsv.academy/bitcoin-essentials)
|
15 |
+
- [Satolearn](https://www.satolearn.com/overview)
|
16 |
+
- [BSV Wiki](https://wiki.bitcoinsv.io/index.php/Main_Page)
|
bsv.md
ADDED
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 1
|
3 |
+
---
|
4 |
+
# The BSV submodule
|
5 |
+
|
6 |
+
sCrypt exports a submodule named `bsv` which is an interface that helps you manage low-level things for the Bitcoin blockchain, such as creating key pairs, building, signing and serializing Bitcoin transactions and more.
|
7 |
+
|
8 |
+
In the context of sCrypt, it is mainly used for managing key pairs and defining custom transaction builders, as demonstrated in [this section](../how-to-deploy-and-call-a-contract/how-to-customize-a-contract-tx.md).
|
9 |
+
|
10 |
+
The goal of this section is to guide you through the basics of using the `bsv` submodule.
|
11 |
+
|
12 |
+
## Importing
|
13 |
+
|
14 |
+
You can import the `bsv` submodule like so:
|
15 |
+
|
16 |
+
```ts
|
17 |
+
import { bsv } from 'scrypt-ts'
|
18 |
+
```
|
19 |
+
|
20 |
+
## Private Keys
|
21 |
+
|
22 |
+
A private key object is essentially just a wrapper around a 256-bit integer.
|
23 |
+
|
24 |
+
You can generate a Bitcoin private key from a random value:
|
25 |
+
|
26 |
+
```ts
|
27 |
+
const privKey = bsv.PrivateKey.fromRandom()
|
28 |
+
// Same as: const privKey = bsv.PrivateKey.fromRandom(bsv.Network.mainnet)
|
29 |
+
```
|
30 |
+
|
31 |
+
This will generate a private key for the Bitcoin main network. To create a key for the test network (also referred to as "testnet"), do the following instead:
|
32 |
+
|
33 |
+
```ts
|
34 |
+
const privKey = bsv.PrivateKey.fromRandom(bsv.Networks.testnet)
|
35 |
+
```
|
36 |
+
|
37 |
+
The main difference between a mainnet and a testnet key is how they get serialized. Check out [this page](https://wiki.bitcoinsv.io/index.php/Wallet_Import_Format_(WIF)) which explains this in detail.
|
38 |
+
|
39 |
+
You can also create key object from serialized keys:
|
40 |
+
```ts
|
41 |
+
const privKey = bsv.PrivateKey.fromWIF('cVDFHtcTU1wn92AkvTyDbtVqyUJ1SFQTEEanAWJ288xvA7TEPDcZ')
|
42 |
+
const privKey2 = bsv.PrivateKey.fromString('e3a9863f4c43576cdc316986ba0343826c1e0140b0156263ba6f464260456fe8')
|
43 |
+
```
|
44 |
+
|
45 |
+
You can see the decimal value of the private key the following way:
|
46 |
+
```ts
|
47 |
+
console.log(privKey.bn.toString())
|
48 |
+
```
|
49 |
+
|
50 |
+
> **Warning**
|
51 |
+
> Private keys should be carefully stored and never be publicly revealed. Otherwise it may lead to loss of funds.
|
52 |
+
|
53 |
+
## Public Keys
|
54 |
+
|
55 |
+
A public key is a key that is derived from a private key and can be shared publicly. Mathematically, a public key is a point on the default elliptic curve that Bitcoin uses, named [`SECP256K1`](https://wiki.bitcoinsv.io/index.php/Secp256k1). It is the curve's base point multiplied by the value of the private key.
|
56 |
+
|
57 |
+
You can get the public key corresponding to a private key the following way:
|
58 |
+
|
59 |
+
```ts
|
60 |
+
const privKey = bsv.PrivateKey.fromRandom(bsv.Networks.testnet)
|
61 |
+
const pubKey = privKey.toPublicKey()
|
62 |
+
```
|
63 |
+
|
64 |
+
Same as with private key you can serialize and deserialize public keys:
|
65 |
+
|
66 |
+
```ts
|
67 |
+
const pubKey = bsv.PublicKey.fromHex('03a687b08533e37d5a6ff5c8b54a9869d4def9bdc2a4bf8c3a5b3b34d8934ccd17')
|
68 |
+
|
69 |
+
console.log(pubKey.toHex())
|
70 |
+
// 03a687b08533e37d5a6ff5c8b54a9869d4def9bdc2a4bf8c3a5b3b34d8934ccd17
|
71 |
+
```
|
72 |
+
|
73 |
+
## Addresses
|
74 |
+
|
75 |
+
You can get a Bitcoin address from either the private key or the public key:
|
76 |
+
|
77 |
+
```ts
|
78 |
+
const privKey = bsv.PrivateKey.fromRandom(bsv.Networks.testnet)
|
79 |
+
const pubKey = privKey.toPublicKey()
|
80 |
+
|
81 |
+
console.log(privKey.toAddress())
|
82 |
+
// mxRjX2uxHHmS4rdSYcmCcp2G91eseb5PpF
|
83 |
+
console.log(pubKey.toAddress())
|
84 |
+
// mxRjX2uxHHmS4rdSYcmCcp2G91eseb5PpF
|
85 |
+
```
|
86 |
+
|
87 |
+
Read [this wiki page](https://wiki.bitcoinsv.io/index.php/Bitcoin_address) for more information on how Bitcoin addresses get constructed.
|
88 |
+
|
89 |
+
## Hash Functions
|
90 |
+
|
91 |
+
The `bsv` submodule offers various hash functions that are commonly used in Bitcoin. You can use them like so:
|
92 |
+
|
93 |
+
```ts
|
94 |
+
const hashString = bsv.crypto.Hash.sha256(Buffer.from('this is the data I want to hash')).toString('hex')
|
95 |
+
console.log(hashString)
|
96 |
+
// f88eec7ecabf88f9a64c4100cac1e0c0c4581100492137d1b656ea626cad63e3
|
97 |
+
```
|
98 |
+
|
99 |
+
The hash functions available in the `bsv` submodule are:
|
100 |
+
|
101 |
+
| Hash Function | Output Length | Description |
|
102 |
+
|---------------|--------------|------------------------------------------------------------|
|
103 |
+
| sha256 | 32 bytes | The SHA256 hash. |
|
104 |
+
| sha256sha256 | 32 bytes | The SHA256 hash of the SHA256 hash. Used for blocks and transactions. |
|
105 |
+
| sha512 | 64 bytes | The SHA512 hash. Commonly used in applications. |
|
106 |
+
| sha1 | 20 bytes | The SHA1 hash. |
|
107 |
+
| ripemd160 | 20 bytes | The RIPEMD160 hash. |
|
108 |
+
| sha256ripemd160 | 20 bytes | The RIPEMD160 hash of the SHA256 hash. Used in Bitcoin addresses. |
|
109 |
+
|
110 |
+
Note however, that these [bsv.js hash functions](https://github.com/moneybutton/bsv/blob/master/lib/hash.js) should not be confused with [sCrypt's native hash functions](https://scrypt.io/docs/reference/#hashing-functions). These functions cannot be used in a smart contract method.
|
111 |
+
|
112 |
+
## Constructing Transactions
|
113 |
+
|
114 |
+
The `bsv` submodule offers a flexible system for constructing Bitcoin transactions. Users are able to define scripts, transaction inputs and outputs, and a whole transaction including its metadata. For a complete description of Bitcoins transaction format, please read [this wiki page](https://wiki.bitcoinsv.io/index.php/Bitcoin_Transactions).
|
115 |
+
|
116 |
+
As an exercise let's construct a simple [P2PKH](https://wiki.bitcoinsv.io/index.php/Bitcoin_Transactions#Pay_to_Public_Key_Hash_.28P2PKH.29) transaction from scratch and sign it.
|
117 |
+
|
118 |
+
> **Note:**
|
119 |
+
> As you will notice further in these docs, most of these steps won't be needed in a regular smart contract development workflow as sCrypt already does a lot of heavy lifting for you. This section serves more as a deeper look on what is happening under the hood.
|
120 |
+
|
121 |
+
You can create an empty transaction like this:
|
122 |
+
```ts
|
123 |
+
let tx = new bsv.Transaction()
|
124 |
+
```
|
125 |
+
|
126 |
+
Because the transaction will need an input that provides it with some funds, we can use the `from` function to add one that unlocks the specified [UTXO](https://wiki.bitcoinsv.io/index.php/UTXO):
|
127 |
+
|
128 |
+
```ts
|
129 |
+
let tx = new bsv.Transaction()
|
130 |
+
.from({
|
131 |
+
// TXID that contains the output you want to unlock:
|
132 |
+
txId: 'f50b8c6dedea6a4371d17040a9e8d2ea73d369177737fb9f47177fbda7d4d387',
|
133 |
+
// Index of the UTXO:
|
134 |
+
outputIndex: 0,
|
135 |
+
// Script of the UTXO. In this case it's a regular P2PKH script:
|
136 |
+
script: bsv.Script.fromASM('OP_DUP OP_HASH160 fde69facc20be6eee5ebf5f0ae96444106a0053f OP_EQUALVERIFY OP_CHECKSIG').toHex(),
|
137 |
+
// Value locked in the UTXO in satoshis:
|
138 |
+
satoshis: 99904
|
139 |
+
})
|
140 |
+
```
|
141 |
+
|
142 |
+
Now, the transaction needs an output that will pay to the address `mxXPxaRvFE3178Cr6KK7nrQ76gxjvBQ4UQ` in our example:
|
143 |
+
|
144 |
+
```ts
|
145 |
+
let tx = new bsv.Transaction()
|
146 |
+
.from({
|
147 |
+
// TXID that contains the output you want to unlock:
|
148 |
+
txId: 'f50b8c6dedea6a4371d17040a9e8d2ea73d369177737fb9f47177fbda7d4d387',
|
149 |
+
// Index of the UTXO:
|
150 |
+
outputIndex: 0,
|
151 |
+
// Script of the UTXO. In this case it's a regular P2PKH script:
|
152 |
+
script: bsv.Script.fromASM('OP_DUP OP_HASH160 fde69facc20be6eee5ebf5f0ae96444106a0053f OP_EQUALVERIFY OP_CHECKSIG').toHex(),
|
153 |
+
// Value locked in the UTXO in satoshis:
|
154 |
+
satoshis: 99904
|
155 |
+
}).addOutput(
|
156 |
+
new bsv.Transaction.Output({
|
157 |
+
script: bsv.Script.buildPublicKeyHashOut('mxXPxaRvFE3178Cr6KK7nrQ76gxjvBQ4UQ'),
|
158 |
+
satoshis: 99804,
|
159 |
+
})
|
160 |
+
)
|
161 |
+
```
|
162 |
+
|
163 |
+
Notice how the output value is 100 less than the value of the UTXO we're unlocking. This difference is the [transaction fee](https://wiki.bitcoinsv.io/index.php/Transaction_fees) (sometimes also called the miner fee).
|
164 |
+
|
165 |
+
### Signing
|
166 |
+
|
167 |
+
OK, now that we have the transaction constructed, it's time to sign it. First, we need to seal the transaction, so it will be ready to sign. Then we call the `sign` function, which takes the private key that can unlock the UTXO we passed to the `from` function. In our example, this is the private key that corresponds to the address `n4fTXc2kaKXHyaxmuH5FTKiJ8Tr4fCPHFy`:
|
168 |
+
|
169 |
+
```ts
|
170 |
+
tx = tx.seal().sign('cNSb8V7pRt6r5HrPTETq2Li2EWYEjA7EcQ1E8V2aGdd6UzN9EuMw')
|
171 |
+
```
|
172 |
+
|
173 |
+
Viola! Thats it. This will add the necessary data to the transaction's input script. That being the signature along with the public key of our signing key.
|
174 |
+
|
175 |
+
Now our transaction is ready to be posted to the blockchain. You can serialize the transaction the following way:
|
176 |
+
|
177 |
+
```ts
|
178 |
+
console.log(tx.serialize())
|
179 |
+
```
|
180 |
+
|
181 |
+
For broadcasting, you can use any provider you like. For demo purposes you can simply paste the serialized transaction [here](https://test.whatsonchain.com/broadcast).
|
182 |
+
|
183 |
+
### OP_RETURN Scripts
|
184 |
+
|
185 |
+
In case you would like to put some arbitrary data on-chain, without any locking logic, you can use transaction outputs with an [OP_RETURN](https://wiki.bitcoinsv.io/index.php/OP_RETURN) script.
|
186 |
+
|
187 |
+
An example of an OP_RETURN script written in ASM format is this:
|
188 |
+
|
189 |
+
```
|
190 |
+
OP_FALSE OP_RETURN 734372797074
|
191 |
+
```
|
192 |
+
|
193 |
+
In effect, the opcodes `OP_FALSE OP_RETURN` will make the script unspendable. After them we can insert arbitrary chunks of data. The `734372797074` is actually the string `sCrypt` encoded as an `utf-8` hexadecimal string.
|
194 |
+
|
195 |
+
```js
|
196 |
+
console.log(Buffer.from('sCrypt').toString('hex'))
|
197 |
+
// 734372797074
|
198 |
+
```
|
199 |
+
|
200 |
+
An OP_RETURN script can also contain more than a single chunk of data:
|
201 |
+
```
|
202 |
+
OP_FALSE OP_RETURN 48656c6c6f 66726f6d 734372797074
|
203 |
+
```
|
204 |
+
|
205 |
+
The `bsv` submodule offers a convenient function to construct such scripts:
|
206 |
+
|
207 |
+
```ts
|
208 |
+
const opRetScript: bsv.Script = bsv.Script.buildSafeDataOut(['Hello', 'from', 'sCrypt'])
|
209 |
+
```
|
210 |
+
|
211 |
+
We can add the resulting `bsv.Script` object to an output as we showed [above](#constructing-transactions).
|
212 |
+
|
213 |
+
|
214 |
+
## References
|
215 |
+
|
216 |
+
- Take a look at the full [`bsv` submodule reference](../reference/modules/bsv) for a full list of what functions it provides.
|
217 |
+
- As the `bsv` submodule is based on MoneyButton's library implementation, take a look at their [video tutorial series](https://www.youtube.com/watch?v=bkGiCjYBpJE&list=PLwj1dNv7vWsMrjrWeiQEelbKTI3Lrmvqp&index=1). Although do keep in mind that some things might be slightly different as it's an old series.
|
built-ins.md
ADDED
@@ -0,0 +1,716 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 4
|
3 |
+
---
|
4 |
+
|
5 |
+
# Built-ins
|
6 |
+
|
7 |
+
## Global Functions
|
8 |
+
|
9 |
+
The following functions come with `sCrypt`.
|
10 |
+
|
11 |
+
### Assert
|
12 |
+
|
13 |
+
- `assert(condition: boolean, errorMsg?: string)` Throw an `Error` with the optional error message if `condition` is `false`. Otherwise, nothing happens.
|
14 |
+
|
15 |
+
```ts
|
16 |
+
assert(1n === 1n) // nothing happens
|
17 |
+
assert(1n === 2n) // throws Error('Execution failed')
|
18 |
+
assert(false, 'hello') // throws Error('Execution failed, hello')
|
19 |
+
```
|
20 |
+
|
21 |
+
### Fill
|
22 |
+
|
23 |
+
- `fill(value: T, length: number): T[length] ` Returns an `FixedArray` with all `size` elements set to `value`, where `value` can be any type.
|
24 |
+
|
25 |
+
:::note
|
26 |
+
`length` must be a [compiled-time constant](./how-to-write-a-contract.md#compile-time-constant).
|
27 |
+
:::
|
28 |
+
|
29 |
+
|
30 |
+
```ts
|
31 |
+
// good
|
32 |
+
fill(1n, 3) // numeric literal 3
|
33 |
+
fill(1n, M) // const M = 3
|
34 |
+
fill(1n, Demo.N) // `N` is a static readonly property of class `Demo`
|
35 |
+
```
|
36 |
+
|
37 |
+
### Math
|
38 |
+
|
39 |
+
- `abs(a: bigint): bigint` Returns the absolute value of `a`.
|
40 |
+
|
41 |
+
```ts
|
42 |
+
abs(1n) // 1n
|
43 |
+
abs(0n) // 0n
|
44 |
+
abs(-1n) // 1n
|
45 |
+
```
|
46 |
+
|
47 |
+
- `min(a: bigint, b: bigint): bigint` Returns the smallest of `a` and `b`.
|
48 |
+
|
49 |
+
```ts
|
50 |
+
min(1n, 2n) // 1n
|
51 |
+
```
|
52 |
+
|
53 |
+
- `max(a: bigint, b: bigint): bigint` Returns the lagest of `a` and `b`.
|
54 |
+
|
55 |
+
```ts
|
56 |
+
max(1n, 2n) // 2n
|
57 |
+
```
|
58 |
+
|
59 |
+
- `within(x: bigint, min: bigint, max: bigint): boolean` Returns `true` if `x` is within the specified range (left-inclusive and right-exclusive), `false` otherwise.
|
60 |
+
|
61 |
+
```ts
|
62 |
+
within(0n, 0n, 2n) // true
|
63 |
+
within(1n, 0n, 2n) // true
|
64 |
+
within(2n, 0n, 2n) // false
|
65 |
+
```
|
66 |
+
|
67 |
+
### Hashing
|
68 |
+
|
69 |
+
- `ripemd160(a: ByteString): Ripemd160` Returns the [RIPEMD160](https://en.wikipedia.org/wiki/RIPEMD) hash result of `a`.
|
70 |
+
- `sha1(a: ByteString): Sha1` Returns the [SHA1](https://en.wikipedia.org/wiki/SHA-1) hash result of `a`.
|
71 |
+
- `sha256(a: ByteString): Sha256` Returns the [SHA256](https://www.movable-type.co.uk/scripts/sha256.html) hash result of `a`.
|
72 |
+
- `hash160(a: ByteString): Ripemd160` Actually returns `ripemd160(sha256(a))`
|
73 |
+
- `hash256(a: ByteString): Sha256` Actually returns `sha256(sha256(a))`
|
74 |
+
|
75 |
+
### ByteString Operations
|
76 |
+
|
77 |
+
- `int2ByteString(n: bigint, size?: bigint): ByteString` If `size` is omitted, convert `n` is converted to a `ByteString` in [sign-magnitude](https://en.wikipedia.org/wiki/Signed_number_representations#Sign%E2%80%93magnitude) little endian format, with as few bytes as possible (a.k.a., minimally encoded). Otherwise, converts the number `n` to a `ByteString` of the specified size, including the sign bit; fails if the number cannot be accommodated.
|
78 |
+
|
79 |
+
```ts
|
80 |
+
// as few bytes as possible
|
81 |
+
int2ByteString(128n) // '8000', little endian
|
82 |
+
int2ByteString(127n) // '7f'
|
83 |
+
int2ByteString(0n) // ''
|
84 |
+
int2ByteString(-1n) // '81'
|
85 |
+
int2ByteString(-129n) // '8180', little endian
|
86 |
+
|
87 |
+
// specified size
|
88 |
+
int2ByteString(1n, 3n) // '010000', 3 bytes
|
89 |
+
int2ByteString(-129n, 3n) // '810080', 3 bytes
|
90 |
+
|
91 |
+
// Error: -129 cannot fit in 1 byte
|
92 |
+
int2ByteString(-129n, 1n)
|
93 |
+
```
|
94 |
+
|
95 |
+
- `byteString2Int(a: ByteString): bigint` Convert ByteString in sign-magnitude little endian format to bigint.
|
96 |
+
|
97 |
+
```ts
|
98 |
+
byteString2Int(toByteString('8000')) // 128n
|
99 |
+
byteString2Int(toByteString('')) // 0n
|
100 |
+
byteString2Int(toByteString('00')) // 0n
|
101 |
+
byteString2Int(toByteString('81')) // -1n
|
102 |
+
|
103 |
+
byteString2Int(toByteString('010000')) // 1n
|
104 |
+
byteString2Int(toByteString('810080')) // -129n
|
105 |
+
```
|
106 |
+
|
107 |
+
- `len(a: ByteString): number` Returns the byte length of `a`.
|
108 |
+
|
109 |
+
```ts
|
110 |
+
const s1 = toByteString('0011', false) // '0011', 2 bytes
|
111 |
+
len(s1) // 2
|
112 |
+
|
113 |
+
const s2 = toByteString('hello', true) // '68656c6c6f', 5 bytes
|
114 |
+
len(s2) // 5
|
115 |
+
```
|
116 |
+
|
117 |
+
- `reverseByteString(b: ByteString, size: number): ByteString` Returns reversed bytes of `b` which is of `size` bytes. It is often useful when converting a number between little-endian and big-endian.
|
118 |
+
|
119 |
+
:::note
|
120 |
+
`size` must be a [compiled-time constant](./how-to-write-a-contract.md#compile-time-constant).
|
121 |
+
:::
|
122 |
+
|
123 |
+
```ts
|
124 |
+
const s1 = toByteString('793ff39de7e1dce2d853e24256099d25fa1b1598ee24069f24511d7a2deafe6c')
|
125 |
+
reverseByteString(s1, 32) // 6cfeea2d7a1d51249f0624ee98151bfa259d095642e253d8e2dce1e79df33f79
|
126 |
+
```
|
127 |
+
|
128 |
+
- `slice(byteString: ByteString, start: BigInt, end?: BigInt): ByteString` return a substring from `start` to, but not including, `end`. If `end` is not specified, the substring continues to the last byte.
|
129 |
+
|
130 |
+
```ts
|
131 |
+
const message = toByteString('001122')
|
132 |
+
slice(message, 1n) // '1122'
|
133 |
+
slice(message, 1n, 2n) // '11'
|
134 |
+
```
|
135 |
+
|
136 |
+
### Bitwise Operator
|
137 |
+
|
138 |
+
Bigint in the Bitcoin is stored in [signβmagnitude format](https://en.wikipedia.org/wiki/Signed_number_representations#Sign%E2%80%93magnitude), not [two's complement format](https://en.wikipedia.org/wiki/Signed_number_representations#Two's_complement) commonly used. If the operands are all nonnegative, the result of the operation is consistent with TypeScript's bitwise operator, except `~`. Otherwise, the operation results may be inconsistent and thus undefined. It is strongly recommended to **NEVER** apply bitwise operations on negative numbers.
|
139 |
+
|
140 |
+
- `and(x: bigint, y: bigint): bigint` Bitwise AND
|
141 |
+
|
142 |
+
```ts
|
143 |
+
and(13n, 5n) // 5n
|
144 |
+
and(0x0a32c845n, 0x149f72n) // 0x00108840n, 1083456n
|
145 |
+
```
|
146 |
+
|
147 |
+
- `or(x: bigint, y: bigint): bigint` Bitwise OR
|
148 |
+
|
149 |
+
```ts
|
150 |
+
or(13n, 5n) // 13n
|
151 |
+
or(0x0a32c845n, 0x149f72n) // 0xa36df77n, 171368311n
|
152 |
+
```
|
153 |
+
|
154 |
+
- `xor(x: bigint, y: bigint): bigint` Bitwise XOR
|
155 |
+
|
156 |
+
```ts
|
157 |
+
xor(13n, 5n) // 8n
|
158 |
+
xor(0x0a32c845n, 0x149f72n) // 0x0a265737n, 170284855n
|
159 |
+
```
|
160 |
+
|
161 |
+
- `invert(x: bigint): bigint` Bitwise NOT
|
162 |
+
|
163 |
+
```ts
|
164 |
+
invert(13n) // -114n
|
165 |
+
```
|
166 |
+
|
167 |
+
- `lshift(x: bigint, n: bigint): bigint` Arithmetic left shift, returns `x * 2^n`.
|
168 |
+
|
169 |
+
```ts
|
170 |
+
lshift(2n, 3n) // 16n
|
171 |
+
```
|
172 |
+
|
173 |
+
- `rshift(x: bigint, n: bigint): bigint` Arithmetic right shift, returns `x / 2^n`.
|
174 |
+
|
175 |
+
```ts
|
176 |
+
rshift(21n, 3n) // 2n
|
177 |
+
rshift(1024n, 11n) // 0n
|
178 |
+
```
|
179 |
+
|
180 |
+
### Exit
|
181 |
+
|
182 |
+
- `exit(status: boolean): void` Call this function will terminate contract execution. If `status` is `true` then the contract succeeds; otherwise, it fails.
|
183 |
+
|
184 |
+
## `SmartContract` Methods
|
185 |
+
|
186 |
+
The following `@methods` come with the `SmartContract` base class.
|
187 |
+
|
188 |
+
### `compile`
|
189 |
+
|
190 |
+
Function `static async compile(): Promise<TranspileError[]>` compiles the contract and returns transpile errors if compiling fails.
|
191 |
+
|
192 |
+
```ts
|
193 |
+
// returns transpile errors if compiling fails
|
194 |
+
const transpileErrors = await Demo.compile()
|
195 |
+
```
|
196 |
+
|
197 |
+
### `scriptSize`
|
198 |
+
|
199 |
+
Function `get scriptSize(): number` returns the byte length of the contract locking script.
|
200 |
+
|
201 |
+
```ts
|
202 |
+
const demo = new Demo()
|
203 |
+
const size = demo.scriptSize
|
204 |
+
```
|
205 |
+
|
206 |
+
### `loadArtifact`
|
207 |
+
|
208 |
+
Function `static loadArtifact(artifact: MergedArtifact)` loads the contract artifact file in order to rebuild a contract instance, it's usually called at the front end.
|
209 |
+
|
210 |
+
```ts
|
211 |
+
import { TicTacToe } from './contracts/tictactoe';
|
212 |
+
var artifact = require('../artifacts/src/contracts/tictactoe.json');
|
213 |
+
TicTacToe.loadArtifact(artifact);
|
214 |
+
```
|
215 |
+
|
216 |
+
You may visit [here](https://academy.scrypt.io/en/courses/Build-a-Tic-tac-toe-Game-with-sCrypt-614c387bc0974f55df5af1e5/lessons/2/chapters/1) for more details about how to add a front end to a contract.
|
217 |
+
|
218 |
+
### `checkSig`
|
219 |
+
|
220 |
+
Function `checkSig(signature: Sig, publicKey: PubKey): boolean` verifies an ECDSA signature. It takes two inputs: an ECDSA signature and a public key.
|
221 |
+
|
222 |
+
It returns if the signature matches the public key.
|
223 |
+
|
224 |
+
:::caution
|
225 |
+
All signature checking functions (`checkSig` and `checkMultiSig`) follow the [**NULLFAIL** rule](https://github.com/bitcoin/bips/blob/master/bip-0146.mediawiki#NULLFAIL): if the signature is invalid, the entire contract aborts and fails immediately, unless the signature is an empty ByteString, in which case these functions return `false`.
|
226 |
+
:::
|
227 |
+
|
228 |
+
For example, Pay-to-Public-Key-Hash ([P2PKH](https://learnmeabitcoin.com/guide/p2pkh)) can be implemented as below.
|
229 |
+
|
230 |
+
```ts
|
231 |
+
class P2PKH extends SmartContract {
|
232 |
+
// public key hash of the recipient.
|
233 |
+
@prop()
|
234 |
+
readonly pubKeyHash: PubKeyHash
|
235 |
+
|
236 |
+
constructor(pubKeyHash: PubKeyHash) {
|
237 |
+
super(...arguments)
|
238 |
+
this.pubKeyHash = pubKeyHash
|
239 |
+
}
|
240 |
+
|
241 |
+
@method()
|
242 |
+
public unlock(sig: Sig, pubkey: PubKey) {
|
243 |
+
// check if the passed public key belongs to the specified public key hash
|
244 |
+
assert(hash160(pubkey) == this.pubKeyHash, 'public key hashes are not equal')
|
245 |
+
// check signature validity
|
246 |
+
assert(this.checkSig(sig, pubkey), 'signature check failed')
|
247 |
+
}
|
248 |
+
}
|
249 |
+
```
|
250 |
+
|
251 |
+
### `checkMultiSig`
|
252 |
+
|
253 |
+
Function `checkMultiSig(signatures: Sig[], publickeys: PubKey[]): boolean` verifies an array of ECDSA signatures. It takes two inputs: an array of ECDSA signatures and an array of public keys.
|
254 |
+
|
255 |
+
The function compares the first signature against each public key until it finds an ECDSA match. Starting with the subsequent public key, it compares the second signature against each remaining public key until it finds an ECDSA match. The process is repeated until all signatures have been checked or not enough public keys remain to produce a successful result. All signatures need to match a public key. Because public keys are not checked again if they fail any signature comparison, signatures must be placed in the `signatures` array using the same order as their corresponding public keys were placed in the `publickeys` array. If all signatures are valid, `true` is returned, `false` otherwise.
|
256 |
+
|
257 |
+
```ts
|
258 |
+
class MultiSigPayment extends SmartContract {
|
259 |
+
// public key hashes of the 3 recipients
|
260 |
+
@prop()
|
261 |
+
readonly pubKeyHashes: FixedArray<PubKeyHash, 3>
|
262 |
+
|
263 |
+
constructor(pubKeyHashes: FixedArray<PubKeyHash, 3>) {
|
264 |
+
super(...arguments)
|
265 |
+
this.pubKeyHashes = pubKeyHashes
|
266 |
+
}
|
267 |
+
|
268 |
+
@method()
|
269 |
+
public unlock(
|
270 |
+
signatures: FixedArray<Sig, 3>,
|
271 |
+
publicKeys: FixedArray<PubKey, 3>
|
272 |
+
) {
|
273 |
+
// check if the passed public keys belong to the specified public key hashes
|
274 |
+
for (let i = 0; i < 3; i++) {
|
275 |
+
assert(hash160(publicKeys[i]) == this.pubKeyHashes[i], 'public key hash mismatchΒΈ')
|
276 |
+
}
|
277 |
+
// validate signatures
|
278 |
+
assert(this.checkMultiSig(signatures, publicKeys), 'checkMultiSig failed')
|
279 |
+
}
|
280 |
+
}
|
281 |
+
```
|
282 |
+
|
283 |
+
### `buildStateOutput`
|
284 |
+
|
285 |
+
Function `buildStateOutput(amount: bigint): ByteString` creates an output containing the latest state. It takes an input: the number of satoshis in the output.
|
286 |
+
|
287 |
+
```ts
|
288 |
+
class Counter extends SmartContract {
|
289 |
+
// ...
|
290 |
+
|
291 |
+
@method(SigHash.ANYONECANPAY_SINGLE)
|
292 |
+
public incOnChain() {
|
293 |
+
// ... update state
|
294 |
+
|
295 |
+
// construct the new state output
|
296 |
+
const output: ByteString = this.buildStateOutput(this.ctx.utxo.value)
|
297 |
+
|
298 |
+
// ... verify outputs of current tx
|
299 |
+
}
|
300 |
+
}
|
301 |
+
```
|
302 |
+
|
303 |
+
### `buildChangeOutput`
|
304 |
+
|
305 |
+
Function `buildChangeOutput(): ByteString` creates a P2PKH change output. It will calculate the change amount (`this.changeAmount`) automatically, and use the signer's address by default, unless `changeAddress` field is explicitly set in `MethodCallOptions`.
|
306 |
+
|
307 |
+
```ts
|
308 |
+
class Auction extends SmartContract {
|
309 |
+
|
310 |
+
// ...
|
311 |
+
|
312 |
+
@method()
|
313 |
+
public bid(bidder: PubKeyHash, bid: bigint) {
|
314 |
+
|
315 |
+
// ...
|
316 |
+
|
317 |
+
// Auction continues with a higher bidder.
|
318 |
+
const auctionOutput: ByteString = this.buildStateOutput(bid)
|
319 |
+
|
320 |
+
// Refund previous highest bidder.
|
321 |
+
const refundOutput: ByteString = Utils.buildPublicKeyHashOutput(
|
322 |
+
highestBidder,
|
323 |
+
highestBid
|
324 |
+
)
|
325 |
+
let outputs: ByteString = auctionOutput + refundOutput
|
326 |
+
|
327 |
+
// Add change output.
|
328 |
+
outputs += this.buildChangeOutput()
|
329 |
+
|
330 |
+
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs check failed')
|
331 |
+
}
|
332 |
+
}
|
333 |
+
|
334 |
+
const { tx: callTx, atInputIndex } = await auction.methods.bid(
|
335 |
+
PubKeyHash(toHex(publicKeyHashNewBidder)),
|
336 |
+
BigInt(balance + 1),
|
337 |
+
{
|
338 |
+
fromUTXO: getDummyUTXO(balance),
|
339 |
+
changeAddress: addressNewBidder, // specify the change address of method calling tx explicitly
|
340 |
+
} as MethodCallOptions<Auction>
|
341 |
+
)
|
342 |
+
```
|
343 |
+
|
344 |
+
:::note
|
345 |
+
If you use a [customized call tx builder](../how-to-deploy-and-call-a-contract/how-to-customize-a-contract-tx.md), you must explicitly set the change output of the transaction in the builder beforehand. Otherwise, you cannot call `this.changeAmount` or `this.buildChangeOutput` in the contract.
|
346 |
+
:::
|
347 |
+
|
348 |
+
```ts
|
349 |
+
const unsignedTx: bsv.Transaction = new bsv.Transaction()
|
350 |
+
// add inputs and outputs
|
351 |
+
// ...
|
352 |
+
// add change output
|
353 |
+
// otherwise you cannot call `this.changeAmount` and `this.buildChangeOutput` in the contract
|
354 |
+
.change(options.changeAddress);
|
355 |
+
```
|
356 |
+
|
357 |
+
### `fromTx`
|
358 |
+
|
359 |
+
Function `static fromTx(tx: bsv.Transaction, atOutputIndex: number, offchainValues?: Record<string, any>)` creates an instance with its state synchronized to a given transaction output, identified by `tx` the transaction and `atOutputIndex` the output index. It is needed to [create an up-to-date instance of a contract](./../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#create-a-smart-contract-instance-from-a-transaction).
|
360 |
+
|
361 |
+
```ts
|
362 |
+
// create an instance from a transaction output
|
363 |
+
const instance = ContractName.fromTx(tx, atOutputIndex)
|
364 |
+
|
365 |
+
// we're good here, the `instance` is state synchronized with the on-chain transaction
|
366 |
+
```
|
367 |
+
|
368 |
+
If the contract contains @prop's of type `HashedMap` or `HashedSet`, the values of all these properties at this transaction must be passed in the third argument.
|
369 |
+
|
370 |
+
```ts
|
371 |
+
// e.g. the contract has two stateful properties of type `HashedMap` or `HashedSet`
|
372 |
+
// @prop(true) mySet: HashedSet<bigint>
|
373 |
+
// @prop() myMap: HashedMap<bigint, bigint>
|
374 |
+
const instance = ContractName.fromTx(tx, atOutputIndex, {
|
375 |
+
// pass the values of all these properties at the transaction moment
|
376 |
+
'mySet': currentSet,
|
377 |
+
'myMap': currentMap,
|
378 |
+
})
|
379 |
+
```
|
380 |
+
|
381 |
+
### `buildDeployTransaction`
|
382 |
+
|
383 |
+
Function `async buildDeployTransaction(utxos: UTXO[], amount: number, changeAddress?: bsv.Address | string): Promise<bsv.Transaction>` creates a tx to deploy the contract. The first parameter `utxos` represents one or more [P2PKH](https://learnmeabitcoin.com/technical/p2pkh) inputs for paying transaction fees. The second parameter `amount` is the balance of contract output. The last parameter `changeAddress` is optional and represents a change address. Users override it to [cutomize a deployment tx](../how-to-deploy-and-call-a-contract/how-to-customize-a-contract-tx.md#customize) as below.
|
384 |
+
|
385 |
+
|
386 |
+
```ts
|
387 |
+
override async buildDeployTransaction(utxos: UTXO[], amount: number, changeAddress?: bsv.Address | string): Promise<bsv.Transaction> {
|
388 |
+
const deployTx = new bsv.Transaction()
|
389 |
+
// add p2pkh inputs for paying tx fees
|
390 |
+
.from(utxos)
|
391 |
+
// add contract output
|
392 |
+
.addOutput(new bsv.Transaction.Output({
|
393 |
+
script: this.lockingScript,
|
394 |
+
satoshis: amount,
|
395 |
+
}))
|
396 |
+
// add the change output if passing `changeAddress`
|
397 |
+
if (changeAddress) {
|
398 |
+
deployTx.change(changeAddress);
|
399 |
+
if (this._provider) {
|
400 |
+
deployTx.feePerKb(await this.provider.getFeePerKb());
|
401 |
+
}
|
402 |
+
}
|
403 |
+
|
404 |
+
return deployTx;
|
405 |
+
}
|
406 |
+
```
|
407 |
+
|
408 |
+
### `bindTxBuilder`
|
409 |
+
|
410 |
+
Function `bindTxBuilder(methodName: string, txBuilder: MethodCallTxBuilder<SmartContract>):void` binds the customized transaction builder `MethodCallTxBuilder`, which returns a `ContractTransation`, to a contract public `@method` identified by `methodName`.
|
411 |
+
|
412 |
+
```ts
|
413 |
+
|
414 |
+
/**
|
415 |
+
* A transaction builder.
|
416 |
+
* The default transaction builder only supports fixed-format call transactions.
|
417 |
+
* Some complex contracts require a custom transaction builder to successfully call the contract.
|
418 |
+
*/
|
419 |
+
export interface MethodCallTxBuilder<T extends SmartContract> {
|
420 |
+
(current: T, options: MethodCallOptions<T>, ...args: any): Promise<ContractTransaction>
|
421 |
+
}
|
422 |
+
|
423 |
+
|
424 |
+
// bind a customized tx builder for the public method `instance.unlock()`
|
425 |
+
instance.bindTxBuilder("unlock", (options: MethodCallOptions<T>, ...args: any) => {
|
426 |
+
// ...
|
427 |
+
})
|
428 |
+
```
|
429 |
+
|
430 |
+
You may visit [here](../how-to-deploy-and-call-a-contract/how-to-customize-a-contract-tx.md#customize-1) to see more details on how to customize tx builder.
|
431 |
+
|
432 |
+
|
433 |
+
### `multiContractCall`
|
434 |
+
|
435 |
+
When the `@method`s of multiple contracts is called in a transaction, the transaction builders for each contract collectively construct the `ContractTransation`. Function `static async multiContractCall(partialContractTx: ContractTransaction, signer: Signer): Promise<MultiContractTransaction>` signs and broadcasts the final transaction.
|
436 |
+
|
437 |
+
```ts
|
438 |
+
const partialContractTx1 = await counter1.methods.incrementOnChain(
|
439 |
+
{
|
440 |
+
multiContractCall: true,
|
441 |
+
} as MethodCallOptions<Counter>
|
442 |
+
)
|
443 |
+
|
444 |
+
const partialContractTx2 = await counter2.methods.incrementOnChain(
|
445 |
+
{
|
446 |
+
multiContractCall: true,
|
447 |
+
partialContractTx: partialContractTx1
|
448 |
+
} as MethodCallOptions<Counter>
|
449 |
+
);
|
450 |
+
|
451 |
+
const {tx: callTx, nexts} = await SmartContract.multiContractCall(partialContractTx2, signer)
|
452 |
+
|
453 |
+
|
454 |
+
console.log('Counter contract counter1, counter2 called: ', callTx.id)
|
455 |
+
```
|
456 |
+
|
457 |
+
|
458 |
+
|
459 |
+
## Standard Libraries
|
460 |
+
|
461 |
+
`sCrypt` comes with standard libraries that define many commonly used functions.
|
462 |
+
|
463 |
+
### `Utils`
|
464 |
+
|
465 |
+
The `Utils` library provides a set of commonly used utility functions.
|
466 |
+
|
467 |
+
- `static toLEUnsigned(n: bigint, l: bigint): ByteString` Convert the signed integer `n` to an unsigned integer of `l` bytes, in sign-magnitude little endian format.
|
468 |
+
|
469 |
+
```ts
|
470 |
+
Utils.toLEUnsigned(10n, 3n) // '0a0000'
|
471 |
+
Utils.toLEUnsigned(-10n, 2n) // '0a00'
|
472 |
+
```
|
473 |
+
|
474 |
+
- `static fromLEUnsigned(bytes: ByteString): bigint` Convert ByteString to unsigned integer.
|
475 |
+
|
476 |
+
```ts
|
477 |
+
Utils.fromLEUnsigned(toByteString('0a00')) // 10n
|
478 |
+
Utils.fromLEUnsigned(toByteString('8a')) // 138n, actually converts 8a00 to unsigned integer
|
479 |
+
```
|
480 |
+
|
481 |
+
- `static readVarint(buf: ByteString): ByteString` Read a [VarInt](https://learnmeabitcoin.com/technical/varint) field from `buf`.
|
482 |
+
|
483 |
+
```ts
|
484 |
+
Utils.readVarint(toByteString('0401020304')) // '01020304'
|
485 |
+
```
|
486 |
+
|
487 |
+
- `static writeVarint(buf: ByteString): ByteString` Convert `buf` to a [VarInt](https://learnmeabitcoin.com/technical/varint) field, including the preceding length.
|
488 |
+
|
489 |
+
```ts
|
490 |
+
Utils.writeVarint(toByteString('010203')) // '03010203'
|
491 |
+
```
|
492 |
+
|
493 |
+
- `static buildOutput(outputScript: ByteString, outputSatoshis: bigint): ByteString` Build a transaction output with the specified script and satoshi amount.
|
494 |
+
|
495 |
+
```ts
|
496 |
+
const lockingScript = toByteString('01020304')
|
497 |
+
Utils.buildOutput(lockingScript, 1n) // '01000000000000000401020304'
|
498 |
+
```
|
499 |
+
|
500 |
+
- `static buildPublicKeyHashScript(pubKeyHash: PubKeyHash ): ByteString` Build a [Pay to Public Key Hash (P2PKH)](https://wiki.bitcoinsv.io/index.php/Bitcoin_Transactions#Pay_to_Public_Key_Hash_.28P2PKH.29) script from a public key hash.
|
501 |
+
|
502 |
+
```ts
|
503 |
+
const pubKeyHash = PubKeyHash(toByteString('0011223344556677889900112233445566778899'))
|
504 |
+
Utils.buildPublicKeyHashScript(pubKeyHash) // '76a914001122334455667788990011223344556677889988ac'
|
505 |
+
```
|
506 |
+
|
507 |
+
- `static buildPublicKeyHashOutput(pubKeyHash: PubKeyHash, amount: bigint): ByteString` Build a P2PKH output from the public key hash.
|
508 |
+
|
509 |
+
```ts
|
510 |
+
const pubKeyHash = PubKeyHash(toByteString('0011223344556677889900112233445566778899'))
|
511 |
+
Utils.buildPublicKeyHashOutput(pubKeyHash, 1n) // '01000000000000001976a914001122334455667788990011223344556677889988ac'
|
512 |
+
```
|
513 |
+
|
514 |
+
- `static buildOpreturnScript(data: ByteString): ByteString` Build a data-carrying [FALSE OP_RETURN](https://wiki.bitcoinsv.io/index.php/OP_RETURN) script from `data` payload.
|
515 |
+
|
516 |
+
```ts
|
517 |
+
const data = toByteString('hello world', true)
|
518 |
+
Utils.buildOpreturnScript(data) // '006a0b68656c6c6f20776f726c64'
|
519 |
+
```
|
520 |
+
|
521 |
+
### `HashedMap`
|
522 |
+
|
523 |
+
|
524 |
+
`HashedMap` provides a map/hashtable-like data structure. It is different to use `HashedMap` in on-chain and off-chain context.
|
525 |
+
|
526 |
+
#### On-chain
|
527 |
+
|
528 |
+
The main difference between `HashedMap` and other data types weβve [previously introduced](../how-to-write-a-contract/#data-types) is that it does NOT store raw data (i.e., keys and values) in the contract on the blockchain. It stores their hashed values instead, to minimize on-chain storage, which is expensive.
|
529 |
+
|
530 |
+
These guidelines must be followed when using `HashedMap` in a contract `@method`, i.e., on-chain context.
|
531 |
+
|
532 |
+
* Only the following methods can be called.
|
533 |
+
|
534 |
+
- `set(key: K, val: V): HashedMap`: Adds a new element with a specified key and value. If an element with the same key already exists, the element will be updated.
|
535 |
+
- `canGet(key: K, val: V): boolean`: Returns `true` if the specified **key and value pair** exists, otherwise returns `false`.
|
536 |
+
- `has(key: K): boolean`: Returns `true` if the specified key exists, otherwise returns `false`.
|
537 |
+
- `delete(key: K): boolean`: Returns `true` if a key exists and has been removed, otherwise returns `false`.
|
538 |
+
- `clear(): void`: Remove all key and value pairs.
|
539 |
+
- `size: number`: Returns the number of elements.
|
540 |
+
|
541 |
+
:::note
|
542 |
+
`get()` is not listed, since the value itself is not stored and thus must be passed in and verified using `canGet()`.
|
543 |
+
:::
|
544 |
+
|
545 |
+
* The aforementioned methods can only be used in public `@method`s, NOT in non-public `@method`s, including constructors.
|
546 |
+
|
547 |
+
* `HashedMap` can be used as an `@prop`, either stateful or not:
|
548 |
+
|
549 |
+
```ts
|
550 |
+
@prop() map: HashedMap<KeyType, ValueType>; // valid
|
551 |
+
@prop(true) map: HashedMap<KeyType, ValueType> // also valid
|
552 |
+
```
|
553 |
+
|
554 |
+
* It CANNOT be used as a `@method` parameter, regardless of public or not:
|
555 |
+
|
556 |
+
```ts
|
557 |
+
@method public unlock(map: HashedMap<KeyType, ValueType>) // invalid as a parameter type
|
558 |
+
@method foo(map: HashedMap<KeyType, ValueType>) // invalid as a parameter type
|
559 |
+
```
|
560 |
+
|
561 |
+
* No nesting is allowed currently. That is, key and value cannot contain a `HashedMap`.
|
562 |
+
```ts
|
563 |
+
type Map1 = HashedMap<KeyType1, ValueType1>
|
564 |
+
HashedMap<KeyType2, Map1> // invalid
|
565 |
+
HashedMap<Map1, ValueType2> // invalid
|
566 |
+
|
567 |
+
type KeyType = {
|
568 |
+
key1: KeyType1
|
569 |
+
key2: KeyType2
|
570 |
+
}
|
571 |
+
HashedMap<KeyType, ValueType> // valid
|
572 |
+
```
|
573 |
+
|
574 |
+
A full example may look like this:
|
575 |
+
|
576 |
+
```ts
|
577 |
+
class MyContract extends SmartContract {
|
578 |
+
@prop(true)
|
579 |
+
myMap: HashedMap<bigint, bigint>;
|
580 |
+
|
581 |
+
// HashedMap can be a parameter in constructor
|
582 |
+
constructor(map: HashedMap<bigint, bigint>) {
|
583 |
+
// assignment is ok, but not calling method
|
584 |
+
this.myMap = map;
|
585 |
+
}
|
586 |
+
|
587 |
+
@method()
|
588 |
+
public unlock(key: bigint, val: bigint) {
|
589 |
+
this.myMap.set(key, val);
|
590 |
+
assert(this.myMap.has(key));
|
591 |
+
assert(this.myMap.canGet(key, val));
|
592 |
+
assert(this.myMap.delete(key));
|
593 |
+
assert(!this.myMap.has(key));
|
594 |
+
}
|
595 |
+
}
|
596 |
+
```
|
597 |
+
|
598 |
+
#### Off-chain
|
599 |
+
|
600 |
+
`HashedMap` acts just like the JavaScript/TypeScript [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) when used in off-chain code (that is, not in a contract's `@method`). For example, you can create an instance like this:
|
601 |
+
|
602 |
+
```ts
|
603 |
+
// create an empty map
|
604 |
+
let hashedMap = new HashedMap<bigint, ByteString>();
|
605 |
+
|
606 |
+
// create from (key,value) pairs
|
607 |
+
let hashedMap1 = new HashedMap([['key1', 'value1'], ['key2', 'value2']]);
|
608 |
+
```
|
609 |
+
|
610 |
+
Also, you can call its functions like this:
|
611 |
+
|
612 |
+
```ts
|
613 |
+
hashedMap.set(key, value);
|
614 |
+
const v = hashedMap.get(key); // <----
|
615 |
+
hashedMap.has(key);
|
616 |
+
hashedMap.delete(key);
|
617 |
+
...
|
618 |
+
```
|
619 |
+
:::note
|
620 |
+
`get()` can be called since the HashedMap stores the original key and value off chain.
|
621 |
+
:::
|
622 |
+
|
623 |
+
Only when the key is an object is `HashedMap` different from `Map`. `HashedMap` will treat two keys the same if they have the same values, while `Map` will only if they reference the same object. For instance:
|
624 |
+
|
625 |
+
```ts
|
626 |
+
interface ST {
|
627 |
+
a: bigint;
|
628 |
+
}
|
629 |
+
|
630 |
+
let map = new Map<ST, bigint>();
|
631 |
+
map.set({a: 1n}, 1n);
|
632 |
+
map.set({a: 1n}, 2n);
|
633 |
+
console.log(map.size); // output β2β cause two keys {a: 1n} reference differently
|
634 |
+
console.log(map.get({a: 1n})); // output βundefinedβ
|
635 |
+
|
636 |
+
|
637 |
+
let hashedMap = new HashedMap<ST, bigint>();
|
638 |
+
hashedMap.set({a: 1n}, 1n);
|
639 |
+
hashedMap.set({a: 1n}, 2n);
|
640 |
+
console.log(hashedMap.size); // output β1β
|
641 |
+
console.log(hashedMap.get({a: 1n})); // output β2nβ
|
642 |
+
```
|
643 |
+
|
644 |
+
### `HashedSet`
|
645 |
+
|
646 |
+
|
647 |
+
`HashedSet` library provides a set-like data structure. It can be regarded as a special `HashedMap` where a value is the same with its key and is thus omitted. Values are hashed before being stored in contracts on the blockchain, as in `HashedMap`.
|
648 |
+
|
649 |
+
#### On-chain
|
650 |
+
|
651 |
+
When used in public `@method`s, `HashedSet` also has almost all of the same restrictions as `HashedMap`. Except for the methods on its own whitelist that can be called in `@method`s as following:
|
652 |
+
|
653 |
+
- `add(value: T): HashedSet`: Inserts a new element with a specified value in to a set, if there isn't an element with the same value already in the set.
|
654 |
+
|
655 |
+
- `has(value: T): boolean`: Returns `true` if an element with the specified value exists in the set, otherwise returns `false`.
|
656 |
+
|
657 |
+
- `delete(value: T): boolean`: Returns `true` if an element in the Set existed and has been removed, or false if the element does not exist.
|
658 |
+
|
659 |
+
- `clear(): void`: Delete all entries of the set.
|
660 |
+
|
661 |
+
- `size: number`: Returns the size of set, i.e. the number of the entries it contains.
|
662 |
+
|
663 |
+
|
664 |
+
#### Off-chain
|
665 |
+
|
666 |
+
`HashedSet` can be used the same as a JavaScript `Set` in off-chain code .
|
667 |
+
|
668 |
+
```ts
|
669 |
+
let hashedSet = new HashedSet<bigint>()
|
670 |
+
hashedSet.add(1n);
|
671 |
+
hashedSet.has(1n);
|
672 |
+
hashedSet.delete(1n);
|
673 |
+
...
|
674 |
+
```
|
675 |
+
|
676 |
+
Similar to `HashedMap`, `HashedSet` will treat two objects as identical if their values equal, rather than requiring that they reference to the same object.
|
677 |
+
|
678 |
+
```ts
|
679 |
+
interface ST {
|
680 |
+
a: bigint;
|
681 |
+
}
|
682 |
+
|
683 |
+
let set = new Set<ST>();
|
684 |
+
set.add({a: 1n});
|
685 |
+
set.add({a: 1n});
|
686 |
+
console.log(set.size); // output β2β
|
687 |
+
console.log(set.has({a: 1n})); // output βfalseβ
|
688 |
+
|
689 |
+
|
690 |
+
let hashedSet = new HashedSet<ST, bigint>();
|
691 |
+
hashedSet.add({a: 1n});
|
692 |
+
hashedSet.add({a: 1n});
|
693 |
+
console.log(hashedSet.size); // output β1β
|
694 |
+
console.log(hashedSet.has({a: 1n})); // output βtrueβ
|
695 |
+
```
|
696 |
+
|
697 |
+
### `Constants`
|
698 |
+
|
699 |
+
`Constants` defines some commonly used constant values.
|
700 |
+
|
701 |
+
```ts
|
702 |
+
class Constants {
|
703 |
+
// number of string to denote input sequence
|
704 |
+
static readonly InputSeqLen: bigint = BigInt(4);
|
705 |
+
// number of string to denote output value
|
706 |
+
static readonly OutputValueLen: bigint = BigInt(8);
|
707 |
+
// number of string to denote a public key (compressed)
|
708 |
+
static readonly PubKeyLen: bigint = BigInt(33);
|
709 |
+
// number of string to denote a public key hash
|
710 |
+
static readonly PubKeyHashLen: bigint = BigInt(20);
|
711 |
+
// number of string to denote a tx id
|
712 |
+
static readonly TxIdLen: bigint = BigInt(32);
|
713 |
+
// number of string to denote a outpoint
|
714 |
+
static readonly OutpointLen: bigint = BigInt(36);
|
715 |
+
}
|
716 |
+
```
|
call-deployed.md
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 5
|
3 |
+
---
|
4 |
+
|
5 |
+
# Interact with a Deployed Contract
|
6 |
+
|
7 |
+
## Overview
|
8 |
+
In this tutorial, we will interact with a deployed smart contract by calling its public method, in a separate process or by a different party.
|
9 |
+
We need to create an instance corresponding to the deployed contract on chain.
|
10 |
+
|
11 |
+
## The Smart Contract
|
12 |
+
|
13 |
+
We will reuse [the `Counter` contract](../how-to-write-a-contract/stateful-contract.md#create-a-stateful-contract).
|
14 |
+
|
15 |
+
```ts
|
16 |
+
export class Counter extends SmartContract {
|
17 |
+
|
18 |
+
@prop(true)
|
19 |
+
count: bigint
|
20 |
+
|
21 |
+
constructor(count: bigint) {
|
22 |
+
super(...arguments)
|
23 |
+
this.count = count
|
24 |
+
}
|
25 |
+
|
26 |
+
@method()
|
27 |
+
public incrementOnChain() {
|
28 |
+
// Increment counter.
|
29 |
+
this.increment()
|
30 |
+
|
31 |
+
// Ensure next output will contain this contracts code w
|
32 |
+
// the updated count property.
|
33 |
+
const amount: bigint = this.ctx.utxo.value
|
34 |
+
const outputs: ByteString = this.buildStateOutput(amount) + this.buildChangeOutput()
|
35 |
+
assert(this.ctx.hashOutputs == hash256(outputs), 'hashOutputs mismatch')
|
36 |
+
}
|
37 |
+
|
38 |
+
@method()
|
39 |
+
increment(): void {
|
40 |
+
this.count++
|
41 |
+
}
|
42 |
+
}
|
43 |
+
```
|
44 |
+
|
45 |
+
## Deploy
|
46 |
+
|
47 |
+
To deploy the smart contract, we define the following function:
|
48 |
+
|
49 |
+
```ts
|
50 |
+
async function deploy(initialCount = 100n): Promise<string> {
|
51 |
+
const instance = new Counter(initialCount)
|
52 |
+
await instance.connect(getDefaultSigner())
|
53 |
+
const tx = await instance.deploy(1)
|
54 |
+
console.log(`Counter deployed: ${tx.id}, the count is: ${instance.count}`)
|
55 |
+
return tx.id
|
56 |
+
}
|
57 |
+
```
|
58 |
+
|
59 |
+
The function deploys the contract with a balance of 1 satoshi and returns the TXID of the deployed contract.
|
60 |
+
|
61 |
+
## Interact
|
62 |
+
Next, we update our deployed smart contract by calling the following function:
|
63 |
+
|
64 |
+
```ts
|
65 |
+
async function callIncrementOnChain(
|
66 |
+
txId: string,
|
67 |
+
atOutputIndex = 0
|
68 |
+
): Promise<string> {
|
69 |
+
// Fetch TX via provider and reconstruct contract instance.
|
70 |
+
const signer = getDefaultSigner()
|
71 |
+
const tx = await signer.connectedProvider.getTransaction(txId)
|
72 |
+
const instance = Counter.fromTx(tx, atOutputIndex)
|
73 |
+
|
74 |
+
await instance.connect(signer)
|
75 |
+
|
76 |
+
const nextInstance = instance.next()
|
77 |
+
nextInstance.increment()
|
78 |
+
|
79 |
+
const { tx: callTx } = await instance.methods.incrementOnChain({
|
80 |
+
next: {
|
81 |
+
instance: nextInstance,
|
82 |
+
balance: instance.balance,
|
83 |
+
},
|
84 |
+
} as MethodCallOptions<Counter>)
|
85 |
+
console.log(`Counter incrementOnChain called: ${callTx.id}, the count now is: ${nextInstance.count}`)
|
86 |
+
return callTx.id
|
87 |
+
}
|
88 |
+
```
|
89 |
+
|
90 |
+
The function takes as parameters the TXID of the deployed smart contract to [create an instance](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#create-a-smart-contract-instance-from-a-transaction), along with the output index (which is usually 0). It uses the [`DefaultProvider`](../reference/classes/DefaultProvider) to fetch the transaction data from the blockchain. Subsequently, it reconstructs the smart contract instance using the [`fromTx`](../how-to-write-a-contract/built-ins.md#fromtx) function.
|
91 |
+
|
92 |
+
Let's encapsulate the entire process within a main function, designed to deploy the contract and increment its value five times:
|
93 |
+
|
94 |
+
```ts
|
95 |
+
async function main() {
|
96 |
+
await compileContract()
|
97 |
+
let lastTxId = await deploy()
|
98 |
+
for (let i = 0; i < 5; ++i) {
|
99 |
+
lastTxId = await callIncrementOnChain(lastTxId)
|
100 |
+
}
|
101 |
+
}
|
102 |
+
|
103 |
+
(async () => {
|
104 |
+
await main()
|
105 |
+
})()
|
106 |
+
```
|
107 |
+
|
108 |
+
If we execute the code, we should get an output similar to the following:
|
109 |
+
|
110 |
+
```ts
|
111 |
+
Counter deployed: 1cd6eb4ff0a5bd83f06c60c5e9a5c113c6e44fd876096e4e94e04a80fee8c8ca, the count is: 100
|
112 |
+
Counter incrementOnChain called: c5b8d8f37f5d9c089a73a321d58c3ae205087ba21c1e32ed09a1b2fbd4f65330, the count now is: 101
|
113 |
+
Counter incrementOnChain called: c62bb0f187f81dfeb5b70eafe80d549d3b2c6219e16d9575639b4fbdffd1d391, the count now is: 102
|
114 |
+
Counter incrementOnChain called: 9fb217b98324b633d8a0469d6a2478f522c1f40c0b6d806430efe5ae5457ca0e, the count now is: 103
|
115 |
+
Counter incrementOnChain called: 2080ddecc7f7731fc6afd307a57c8b117227755bd7b82eb0bc7cd8b78417ad9a, the count now is: 104
|
116 |
+
Counter incrementOnChain called: de43687fd386e92cd892c18600d473bc38d5adb0cc34bbda892b94c61b5d5eb8, the count now is: 105
|
117 |
+
```
|
118 |
+
|
119 |
+
## Conclusion
|
120 |
+
|
121 |
+
Congratulations! You've now deployed AND interacted with a Bitcoin smart contract.
|
122 |
+
You can see a complete test example in our [boilerplate repository](https://github.com/sCrypt-Inc/boilerplate/blob/master/tests/testnet/counterFromTx.ts).
|
deploy-cli.md
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 3
|
3 |
+
---
|
4 |
+
|
5 |
+
# Deploy Using CLI
|
6 |
+
|
7 |
+
The `deploy` command allows you to deploy an instance of a smart contract to the blockchain. You can simply run the following command in the root of an `sCrypt` project:
|
8 |
+
|
9 |
+
```sh
|
10 |
+
scrypt deploy
|
11 |
+
```
|
12 |
+
|
13 |
+
or
|
14 |
+
|
15 |
+
```sh
|
16 |
+
scrypt d
|
17 |
+
```
|
18 |
+
|
19 |
+
By default, the CLI tool will run a script named `deploy.ts` located in the root of the project. You can also specify a different deployment script using the `--file` or `-f` option.
|
20 |
+
|
21 |
+
```sh
|
22 |
+
scrypt d -f myCustomDeploy.ts
|
23 |
+
```
|
24 |
+
|
25 |
+
If the project was created using sCrypt CLI, it will already have a `deploy.ts` file present (except for [library](../how-to-publish-a-contract.md) projects). If not, the `deploy` command will generate a sample `deploy.ts` file.
|
26 |
+
|
27 |
+
Here's an example of such a deployment file:
|
28 |
+
```ts
|
29 |
+
import { Demoproject } from './src/contracts/demoproject'
|
30 |
+
import { bsv, TestWallet, DefaultProvider, sha256, toByteString, } from 'scrypt-ts'
|
31 |
+
|
32 |
+
import * as dotenv from 'dotenv'
|
33 |
+
|
34 |
+
// Load the .env file
|
35 |
+
dotenv.config()
|
36 |
+
|
37 |
+
// Read the private key from the .env file.
|
38 |
+
// The default private key inside the .env file is meant to be used for the Bitcoin testnet.
|
39 |
+
// See https://scrypt.io/docs/bitcoin-basics/bsv/#private-keys
|
40 |
+
const privateKey = bsv.PrivateKey.fromWIF(process.env.PRIVATE_KEY)
|
41 |
+
|
42 |
+
// Prepare signer.
|
43 |
+
// See https://scrypt.io/docs/how-to-deploy-and-call-a-contract/#prepare-a-signer-and-provider
|
44 |
+
const signer = new TestWallet(privateKey, new DefaultProvider())
|
45 |
+
|
46 |
+
async function main() {
|
47 |
+
// Compile the smart contract.
|
48 |
+
await Demoproject.compile()
|
49 |
+
|
50 |
+
// The amount of satoshis locked in the smart contract:
|
51 |
+
const amount = 100
|
52 |
+
|
53 |
+
// Instantiate the smart contract and pass constructor parameters.
|
54 |
+
const instance = new Demoproject(
|
55 |
+
sha256(toByteString('hello world', true))
|
56 |
+
)
|
57 |
+
|
58 |
+
// Connect to a signer.
|
59 |
+
await instance.connect(signer)
|
60 |
+
|
61 |
+
// Contract deployment.
|
62 |
+
const deployTx = await instance.deploy(amount)
|
63 |
+
console.log('Demoproject contract deployed: ', deployTx.id)
|
64 |
+
}
|
65 |
+
|
66 |
+
main()
|
67 |
+
```
|
68 |
+
|
69 |
+
Upon a successful execution you should see an output like the following:
|
70 |
+
|
71 |
+
```
|
72 |
+
Demoproject contract deployed: 15b8055cfaf9554035f8d3b866f038a04e40b45e28109f1becfe4d0af9f743cd
|
73 |
+
```
|
74 |
+
|
75 |
+
You can take a look at the deployed smart contract using the [WhatsOnChain block explorer](https://test.whatsonchain.com/tx/15b8055cfaf9554035f8d3b866f038a04e40b45e28109f1becfe4d0af9f743cd).
|
76 |
+
In our example, the first output contains the compiled smart contract code.
|
77 |
+
It is indexed using the hash (double SHA-256) of the script: [eb2f10b8f1bd12527f07a5d05b40f06137cbebe4e9ecfb6a4e0fd8a3437e1def](https://test.whatsonchain.com/script/eb2f10b8f1bd12527f07a5d05b40f06137cbebe4e9ecfb6a4e0fd8a3437e1def)
|
escrow.md
ADDED
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 7
|
3 |
+
---
|
4 |
+
|
5 |
+
# Tutorial 7: Escrow
|
6 |
+
|
7 |
+
## Overview
|
8 |
+
|
9 |
+
In this tutorial, we will go over how to create and escrow smart contract with some advanced features, such as a requirement for multiple arbiters and a deadline, after which the buyer can get a refund.
|
10 |
+
|
11 |
+
### What is an escrow smart contract?
|
12 |
+
|
13 |
+
An escrow smart contract is a type of digital agreement that Bitcoin to facilitate transactions between parties in a secure, trustless manner.
|
14 |
+
|
15 |
+
In traditional escrow services, a trusted third party holds assetsβlike money, property, or goodsβon behalf of the transacting parties. The assets are released only when specific conditions are met.
|
16 |
+
|
17 |
+
In the case of an escrow smart contract, the "third party" is the smart contract itself, programmed on the blockchain. The contract is written with the conditions of the transaction, and if they are met, the contract can be unlocked and the recipient(s) get payed.
|
18 |
+
|
19 |
+
### Our implementation
|
20 |
+
|
21 |
+
We will implement a specific type of escrow, called a multi-sig escrow. The participants of this contract are a buyer (Alice), a seller (Bob) and one or more arbiters.
|
22 |
+
|
23 |
+
Suppose Alice want's to buy a specific item from Bob. They don't trust each other, so they decide to use an escrow smart contract. They pick one or more arbiters, which they both trust. The job of the chosen arbiters is to verify, that the item really gets delivered in the right condition. If the conditions are met, the contract will pay the seller, Bob. In the opposite case, Alice gets a refund. Additionally, Alice is also eligible for a refund after a set period of time in the case the arbiters are not responsive.
|
24 |
+
|
25 |
+
## Contract properties
|
26 |
+
|
27 |
+
Let's declare the properties of our smart contract:
|
28 |
+
|
29 |
+
```ts
|
30 |
+
// Number of arbiters chosen.
|
31 |
+
static readonly N_ARBITERS = 3
|
32 |
+
|
33 |
+
// Buyer (Alice) address.
|
34 |
+
@prop()
|
35 |
+
readonly buyerAddr: PubKeyHash
|
36 |
+
|
37 |
+
// Seller (Bob) address.
|
38 |
+
@prop()
|
39 |
+
readonly sellerAddr: PubKeyHash
|
40 |
+
|
41 |
+
// Arbiter public keys.
|
42 |
+
@prop()
|
43 |
+
readonly arbiters: FixedArray<PubKey, typeof MultiSigEscrow.N_ARBITERS>
|
44 |
+
|
45 |
+
// Contract deadline nLocktime value.
|
46 |
+
// Either timestamp or block height.
|
47 |
+
@prop()
|
48 |
+
readonly deadline: bigint
|
49 |
+
```
|
50 |
+
|
51 |
+
## Public method - `confirmPayment`
|
52 |
+
|
53 |
+
The first method of our contract will be `confirmPayment`. This public method will be called if the item was successfully delivered in the right condition.
|
54 |
+
|
55 |
+
The method takes as inputs the buyers signature, along with her public key and the signatures of the arbiters.
|
56 |
+
|
57 |
+
```ts
|
58 |
+
// Buyer and arbiters confirm, that the item was delivered.
|
59 |
+
// Seller gets paid.
|
60 |
+
@method(SigHash.ANYONECANPAY_SINGLE)
|
61 |
+
public confirmPayment(
|
62 |
+
buyerSig: Sig,
|
63 |
+
buyerPubKey: PubKey,
|
64 |
+
arbiterSigs: FixedArray<Sig, typeof MultiSigEscrow.N_ARBITERS>
|
65 |
+
) {
|
66 |
+
// Validate buyer sig.
|
67 |
+
assert(
|
68 |
+
hash160(buyerPubKey) == this.buyerAddr,
|
69 |
+
'invalid public key for buyer'
|
70 |
+
)
|
71 |
+
assert(
|
72 |
+
this.checkSig(buyerSig, buyerPubKey),
|
73 |
+
'buyer signature check failed'
|
74 |
+
)
|
75 |
+
|
76 |
+
// Validate arbiter sigs.
|
77 |
+
assert(
|
78 |
+
this.checkMultiSig(arbiterSigs, this.arbiters),
|
79 |
+
'arbiters checkMultiSig failed'
|
80 |
+
)
|
81 |
+
|
82 |
+
// Ensure seller gets payed.
|
83 |
+
const amount = this.ctx.utxo.value
|
84 |
+
const out = Utils.buildPublicKeyHashOutput(this.sellerAddr, amount)
|
85 |
+
assert(hash256(out) == this.ctx.hashOutputs, 'hashOutputs mismatch')
|
86 |
+
}
|
87 |
+
```
|
88 |
+
|
89 |
+
The method validates all signatures are correct and ensures the **seller** receives the funds.
|
90 |
+
|
91 |
+
## Public method - `refund`
|
92 |
+
|
93 |
+
Next, we implement the public method `refund`. If the delivery wasn't successful or there is something wrong with the item and needs to be sent back, the buyer is eligible for a refund.
|
94 |
+
|
95 |
+
The method again takes as inputs the buyers signature, along with her public key and the signatures of the arbiters.
|
96 |
+
|
97 |
+
```ts
|
98 |
+
// Regular refund. Needs arbiters agreement.
|
99 |
+
@method()
|
100 |
+
public refund(
|
101 |
+
buyerSig: Sig,
|
102 |
+
buyerPubKey: PubKey,
|
103 |
+
arbiterSigs: FixedArray<Sig, typeof MultiSigEscrow.N_ARBITERS>
|
104 |
+
) {
|
105 |
+
// Validate buyer sig.
|
106 |
+
assert(
|
107 |
+
hash160(buyerPubKey) == this.buyerAddr,
|
108 |
+
'invalid public key for buyer'
|
109 |
+
)
|
110 |
+
assert(
|
111 |
+
this.checkSig(buyerSig, buyerPubKey),
|
112 |
+
'buyer signature check failed'
|
113 |
+
)
|
114 |
+
|
115 |
+
// Validate arbiter sigs.
|
116 |
+
assert(
|
117 |
+
this.checkMultiSig(arbiterSigs, this.arbiters),
|
118 |
+
'arbiters checkMultiSig failed'
|
119 |
+
)
|
120 |
+
|
121 |
+
// Ensure buyer gets refund.
|
122 |
+
const amount = this.ctx.utxo.value
|
123 |
+
const out = Utils.buildPublicKeyHashOutput(this.buyerAddr, amount)
|
124 |
+
assert(hash256(out) == this.ctx.hashOutputs, 'hashOutputs mismatch')
|
125 |
+
}
|
126 |
+
```
|
127 |
+
|
128 |
+
The method validates all signatures are correct and ensures the **buyer** receives the refund.
|
129 |
+
|
130 |
+
## Public method - `refundDeadline`
|
131 |
+
|
132 |
+
Lastly, we implement the `refundDeadline` method. This method can be called, after the specified contract deadline has been reached. After the deadline, the buyer can receive the refund, even without the arbiters agreement.
|
133 |
+
|
134 |
+
The method takes as inputs in the buyers signature, along with her public key.
|
135 |
+
|
136 |
+
```ts
|
137 |
+
// Deadline for delivery. If reached, the buyer gets refunded.
|
138 |
+
@method()
|
139 |
+
public refundDeadline(buyerSig: Sig, buyerPubKey: PubKey) {
|
140 |
+
assert(
|
141 |
+
hash160(buyerPubKey) == this.buyerAddr,
|
142 |
+
'invalid public key for buyer'
|
143 |
+
)
|
144 |
+
assert(
|
145 |
+
this.checkSig(buyerSig, buyerPubKey),
|
146 |
+
'buyer signature check failed'
|
147 |
+
)
|
148 |
+
|
149 |
+
// Require nLocktime enabled https://wiki.bitcoinsv.io/index.php/NLocktime_and_nSequence
|
150 |
+
assert(
|
151 |
+
this.ctx.sequence < UINT_MAX,
|
152 |
+
'require nLocktime enabled'
|
153 |
+
)
|
154 |
+
|
155 |
+
// Check if using block height.
|
156 |
+
if (this.deadline < LOCKTIME_BLOCK_HEIGHT_MARKER) {
|
157 |
+
// Enforce nLocktime field to also use block height.
|
158 |
+
assert(
|
159 |
+
this.ctx.locktime < LOCKTIME_BLOCK_HEIGHT_MARKER
|
160 |
+
)
|
161 |
+
}
|
162 |
+
assert(this.ctx.locktime >= this.deadline, 'deadline not yet reached')
|
163 |
+
|
164 |
+
// Ensure buyer gets refund.
|
165 |
+
const amount = this.ctx.utxo.value
|
166 |
+
const out = Utils.buildPublicKeyHashOutput(this.buyerAddr, amount)
|
167 |
+
assert(hash256(out) == this.ctx.hashOutputs, 'hashOutputs mismatch')
|
168 |
+
}
|
169 |
+
```
|
170 |
+
|
171 |
+
The method checks the buyers signature validity. It also checks the transaction nLocktime value, to ensure it can be accepted by miners only after the deadline.
|
172 |
+
|
173 |
+
## Conclusion
|
174 |
+
|
175 |
+
Congratulations! You have completed the escrow tutorial!
|
176 |
+
|
177 |
+
The full code along with [tests](https://github.com/sCrypt-Inc/boilerplate/blob/master/tests/local/multisigEscrow.test.ts) can be found in sCrypt's [boilerplate repository](https://github.com/sCrypt-Inc/boilerplate/blob/master/src/contracts/multisigEscrow.ts).
|
178 |
+
|
ethereum-devs.md
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 12
|
3 |
+
---
|
4 |
+
|
5 |
+
# sCrypt for Ethereum Developers
|
6 |
+
|
7 |
+
# Smart contracts on Bitcoin vs Ethereum
|
8 |
+
Bitcoin and Ethereum are both layer-1 blockchains with [fully programmable smart contracts](https://xiaohuiliu.medium.com/turing-machine-on-bitcoin-7f0ebe0d52b1).
|
9 |
+
However, their designs fundamentally differ.
|
10 |
+
|
11 |
+
Ethereum is a global state machine, whose state consists of all smart contracts deployed on it. Each transaction is an input to the state machine, transitioning it to the next state according to the rules defined in the smart contract the transaction calls. The design imposes severe limitations on scalability, since transactions must be sequentially processed due to potential race conditions.
|
12 |
+
|
13 |
+
In Bitcoin, transaction processing is independent of each other since all information needed is localized. There is no shared global state. Bitcoin is maximally parallelizable by design.
|
14 |
+
|
15 |
+
Detailed side-by-side comparison can be found [here](ttps://xiaohuiliu.medium.com/bitcoin-vs-ethereum-smart-contracts-921e0a12b043), which is concisely summarized below.
|
16 |
+
|
17 |
+
|| Ethereum | Bitcoin |
|
18 |
+
|---|---|---|
|
19 |
+
| Execution Environment | [Ethereum Virtual Machine](https://ethereum.org/en/developers/docs/evm/) (EVM) | [Bitcoin Virtual Machine](https://xiaohuiliu.medium.com/introduction-to-bitcoin-smart-contracts-9c0ea37dc757) (BVM)|
|
20 |
+
| Model | Account | [UTXO](./overview.md#how-do-bitcoin-smart-contracts-work) |
|
21 |
+
| Transaction Fee | $1-10 | $0.00001 |
|
22 |
+
| Transactions Per Second | 15 | 3000+ |
|
23 |
+
| Transaction Processing | Sequential | Parallel |
|
24 |
+
| Scalability | Vertical | Horizontal |
|
25 |
+
| Paradigm | Impure | Pure |
|
26 |
+
|
27 |
+
|
28 |
+
# Smart contract development on Bitcoin vs Ethereum
|
29 |
+
|
30 |
+
Besides unboundedly scalable fundation, Bitcoin also offers superior smart cotnract developer experience.
|
31 |
+
|
32 |
+
The table below shows a comparison of popular Ethereum development tools and their counterparts in the Bitcoin ecosystem.
|
33 |
+
|
34 |
+
There are two noticeable differences.
|
35 |
+
1. Bitcoin smart contract is written in TypeScript, one of the most popular programming languages tens of millions of Web2 developers are already familiar with. They do not have to learn a new niche programming language like Solidity, placing a high barrier to entry. They can reuse all of their favoriate tools, such as Visual Studio Code, [WebStorm](https://www.jetbrains.com/webstorm/), and NPM.
|
36 |
+
2. Ethereum's development tools are **fragmented**. They are developed by different entities, who are often competitors. There is disincentive to make them more interoperable, thus they don't communicate with each other well. By contrast, sCrypt takes a more holistic and systematic approach. It builds a unified full-stack platform that encompasses most tools, from programming language, to framework/libraries. Developed synergistically, they are fully compatible with each other, greatly simplifing and streamlining development process.
|
37 |
+
|
38 |
+
|
39 |
+
|| Ethereum | Bitcoin |
|
40 |
+
|---|---|---|
|
41 |
+
| Programming Language | [Solidity](https://soliditylang.org/) | [sCrypt DSL](https://docs.scrypt.io/) |
|
42 |
+
| Framework | [Hardhat](https://hardhat.org/) / [Truffle](https://trufflesuite.com/truffle/) | [The sCrypt CLI](https://www.npmjs.com/package/scrypt-cli) |
|
43 |
+
| Libraries | [Web3.js](https://web3js.org/#/) / [Ethers.js](https://docs.ethers.org) | [scrypt-ts](https://docs.scrypt.io/how-to-write-a-contract/) |
|
44 |
+
| Developer Platform | [Alchemy](https://www.alchemy.com/) / [Infura](https://www.infura.io/) | [sCrypt](https://scrypt.io) |
|
45 |
+
| IDE | [Remix](https://remix.ethereum.org)[^1] | [Visual Studio Code](https://code.visualstudio.com/) |
|
46 |
+
| Wallet | [MetaMask](https://metamask.io/) | [Sensilet](https://sensilet.com/) |
|
47 |
+
| Block Explorer | [Etherscan](https://etherscan.io/) | [WhatsOnChain](https://whatsonchain.com/) |
|
48 |
+
|
49 |
+
[^1]: Visual Studio Code can also be used for Solidity with various extentions. However, its support is extremely limited compared to that of sCrypt, a TypeScript DSL, which is supported out of box without any extension. For example, [VS Code debugger](./how-to-debug-a-contract.md) has first-class comprehensive support for sCrypt, but does not suppport Solidity.
|
50 |
+
|
51 |
+
## Example Code
|
52 |
+
|
53 |
+
Let's compare a counter smart contract between Solidity and sCrypt.
|
54 |
+
|
55 |
+
```js
|
56 |
+
pragma solidity >=0.7.0 <0.9.0;
|
57 |
+
|
58 |
+
contract Counter {
|
59 |
+
|
60 |
+
int private count;
|
61 |
+
|
62 |
+
constructor(int _initialCount) {
|
63 |
+
count = _initialCount;
|
64 |
+
}
|
65 |
+
|
66 |
+
function incrementCounter() public {
|
67 |
+
count += 1;
|
68 |
+
}
|
69 |
+
|
70 |
+
function getCount() public view returns (int) {
|
71 |
+
return count;
|
72 |
+
}
|
73 |
+
|
74 |
+
}
|
75 |
+
```
|
76 |
+
|
77 |
+
```ts
|
78 |
+
class Counter extends SmartContract {
|
79 |
+
|
80 |
+
@prop(true)
|
81 |
+
count: bigint
|
82 |
+
|
83 |
+
constructor(count: bigint) {
|
84 |
+
super(...arguments)
|
85 |
+
this.count = count
|
86 |
+
}
|
87 |
+
|
88 |
+
@method()
|
89 |
+
public incremenCounter() {
|
90 |
+
this.count++
|
91 |
+
|
92 |
+
assert(hash256(this.buildStateOutput(this.ctx.utxo.value)) == this.ctx.hashOutputs)
|
93 |
+
}
|
94 |
+
|
95 |
+
}
|
96 |
+
```
|
faq.md
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 15
|
3 |
+
---
|
4 |
+
|
5 |
+
# FAQ
|
6 |
+
|
7 |
+
## Broadcast double-spending transactions
|
8 |
+
|
9 |
+
You would mainly get two different errors when broadcasting a double-spending transaction, depending on the status of the transaction you're trying to double-spend.
|
10 |
+
|
11 |
+
- If the transaction you're trying to double-spend is still unconfirmed and in the mempool, the error would be `txn-mempool-conflict`.
|
12 |
+
|
13 |
+

|
14 |
+
|
15 |
+
- If the transaction you're trying to double-spend is already confirmed, the error would be `Missing inputs`.
|
16 |
+
|
17 |
+

|
18 |
+
|
19 |
+
If you encounter these errors when interacting with the dApp, it is mainly because the state of the contract has changed, but your browser has not been updated in time, such as the contract has been called by another user. At this time, it is equivalent to that you are interacting with the contract instance that is not in the latest state, resulting in a double-spending.
|
20 |
+
|
21 |
+
To fix this issue, you generally only need to wait for a few seconds, the browser will automatically obtain the latest contract instance after receiving the contract state-change event, or manually refresh the browser yourself, and then try again.
|
22 |
+
|
23 |
+
If you encounter these errors when running the testnet cases, it is mainly because the provider failed to update your UTXO in time so they returned UTXOs that have already been spent when you request. Using these UTXOs that have been spent to build transactions will result in a double-spending. This situation is not common, it might be because there are too many transactions on the blockchain network at this time, the provider cannot update UTXO timely, or somehow the provider's server load is heavy.
|
24 |
+
|
25 |
+
To fix this issue, you generally only need to wait a few seconds and re-run the test. If it still can't be solved, you can also increase the time gap between sending transactions, for example, add a `sleep` ahead of requesting the UTXO after sending transactions, so that the provider has enough time to update the UTXO.
|
26 |
+
|
27 |
+
## Input string too short
|
28 |
+
|
29 |
+
If you do not set the `PRIVATE_KEY` environment variable in `.env` file before deploying a contract, you would get an `Input string too short` error.
|
30 |
+
|
31 |
+

|
32 |
+
|
33 |
+
Please follow [this guide](./how-to-deploy-and-call-a-contract/faucet.md) to generate a new private key or export the private key from your Sensilet wallet, then fund the private key's address with our [faucet](https://scrypt.io/faucet/).
|
34 |
+
|
35 |
+
## No sufficient utxos
|
36 |
+
|
37 |
+
If you don't fund your private key's address before deploying a contract, you would get a `No sufficient utxos` error.
|
38 |
+
|
39 |
+

|
40 |
+
|
41 |
+
Please fund your address with our [faucet](https://scrypt.io/faucet/) first.
|
faucet.md
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 4
|
3 |
+
---
|
4 |
+
|
5 |
+
# Faucet
|
6 |
+
|
7 |
+
It is highly recommended to test your contract on the [testnet](https://test.whatsonchain.com/) after passing local tests. It ensures that a contract can be successfully deployed and invoked as expected on the blockchain.
|
8 |
+
|
9 |
+
Before deploy and call a contract, you need to have a funded address:
|
10 |
+
|
11 |
+
1. Generate a private key with the following command, after creating a project:
|
12 |
+
|
13 |
+
```sh
|
14 |
+
scrypt project demo
|
15 |
+
cd demo
|
16 |
+
npm install
|
17 |
+
npm run genprivkey
|
18 |
+
```
|
19 |
+
|
20 |
+
The command will generate a private key and store it in a `.env` file in our project's root directory. It also outputs the [Bitcoin address](https://wiki.bitcoinsv.io/index.php/Bitcoin_address) corresponding to our private key.
|
21 |
+
|
22 |
+
2. Fund the private key's address with some testnet coins. You could use this [faucet](https://scrypt.io/faucet) to receive test coins.
|
23 |
+
|
24 |
+

|
25 |
+
|
26 |
+
### Use the Sensilet Wallet
|
27 |
+
|
28 |
+
Alternatively, if you have already installed [Sensilet](https://sensilet.com/), you can extract and use its private key on testnet as follows.
|
29 |
+
|
30 |
+

|
hello-world.md
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 1
|
3 |
+
---
|
4 |
+
|
5 |
+
# Tutorial 1: Hello World
|
6 |
+
|
7 |
+
|
8 |
+
## Overview
|
9 |
+
In this tutorial, we will go over how to quickly create a βHello Worldβ smart contract, deploy and call it.
|
10 |
+
|
11 |
+
## Create a new project
|
12 |
+
|
13 |
+
Make sure [all prerequisite tools](../../installation) are installed.
|
14 |
+
|
15 |
+
Run the following commands to create a new project:
|
16 |
+
|
17 |
+
```sh
|
18 |
+
scrypt project helloworld
|
19 |
+
cd helloworld
|
20 |
+
npm install
|
21 |
+
```
|
22 |
+
|
23 |
+
The resulting project will contain a sample smart contract `src/contracts/helloworld.ts`, along with all the scaffolding. Let's modify it to the following code:
|
24 |
+
|
25 |
+
|
26 |
+
```ts
|
27 |
+
import { assert, ByteString, method, prop, sha256, Sha256, SmartContract } from 'scrypt-ts'
|
28 |
+
|
29 |
+
export class Helloworld extends SmartContract {
|
30 |
+
|
31 |
+
@prop()
|
32 |
+
hash: Sha256;
|
33 |
+
|
34 |
+
constructor(hash: Sha256){
|
35 |
+
super(...arguments);
|
36 |
+
this.hash = hash;
|
37 |
+
}
|
38 |
+
|
39 |
+
@method()
|
40 |
+
public unlock(message: ByteString) {
|
41 |
+
assert(sha256(message) == this.hash, 'Hash does not match')
|
42 |
+
}
|
43 |
+
}
|
44 |
+
```
|
45 |
+
|
46 |
+
The `Helloworld` contract stores the sha256 hash of a message in the contract property `hash`. Only a message which hashes to the value set in `this.hash` will unlock the contract.
|
47 |
+
|
48 |
+
Now letβs look at what is in the smart contract.
|
49 |
+
|
50 |
+
|
51 |
+
- `SmartContract`: all smart contracts must extend the `SmartContract` base class.
|
52 |
+
|
53 |
+
- `@prop`: the [`@prop` decorator](../how-to-write-a-contract/how-to-write-a-contract.md#properties) marks a contract property.
|
54 |
+
|
55 |
+
- `@method`: the [`@method` decorator](../how-to-write-a-contract/how-to-write-a-contract.md#method-decorator) marks a contract method. A [public method](../how-to-write-a-contract/#public-methods) is an entry point to a contract.
|
56 |
+
|
57 |
+
- `assert`: throws an error and makes the method call fail if its first argument is `false`. Here it ensures the passed message hashed to the expected digest.
|
58 |
+
|
59 |
+
|
60 |
+
## Contract Deployment & Call
|
61 |
+
|
62 |
+
Before we deploy the contract, follow [the instruction](../../how-to-deploy-and-call-a-contract/faucet) to fund a Bitcoin key.
|
63 |
+
|
64 |
+
1. To [deploy a smart contract](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#contract-deployment), simply call its `deploy` method.
|
65 |
+
|
66 |
+
2. To [call a smart contract](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#contract-call), call one of its public method.
|
67 |
+
|
68 |
+
Overwrite `deploy.ts` in the root of the project with the following code to deploy and call the `Helloworld` contract:
|
69 |
+
|
70 |
+
```ts
|
71 |
+
import { Helloworld } from './src/contracts/helloworld'
|
72 |
+
import { getDefaultSigner } from './tests/utils/txHelper'
|
73 |
+
import { toByteString, sha256 } from 'scrypt-ts'
|
74 |
+
|
75 |
+
(async () => {
|
76 |
+
|
77 |
+
const message = toByteString('hello world', true)
|
78 |
+
|
79 |
+
await Helloworld.compile()
|
80 |
+
const instance = new Helloworld(sha256(message))
|
81 |
+
|
82 |
+
// connect to a signer
|
83 |
+
await instance.connect(getDefaultSigner())
|
84 |
+
|
85 |
+
// deploy the contract and lock up 42 satoshis in it
|
86 |
+
const deployTx = await instance.deploy(42)
|
87 |
+
console.log('Helloworld contract deployed: ', deployTx.id)
|
88 |
+
|
89 |
+
// contract call
|
90 |
+
const { tx: callTx } = await instance.methods.unlock(message)
|
91 |
+
console.log('Helloworld contract `unlock` called: ', callTx.id)
|
92 |
+
|
93 |
+
})()
|
94 |
+
```
|
95 |
+
|
96 |
+
Run the following command:
|
97 |
+
```
|
98 |
+
npx ts-node deploy.ts
|
99 |
+
```
|
100 |
+
You will see some output like:
|
101 |
+
|
102 |
+

|
103 |
+
|
104 |
+
|
105 |
+
You can view [the deployment transaction](https://test.whatsonchain.com/tx/b10744292358eda2cfae3baae5cd486e30136b086011f7953aed9098f62f4245) using the WhatsOnChain blockchain explorer:
|
106 |
+
|
107 |
+

|
108 |
+
|
109 |
+
|
110 |
+
You can also view [the calling transaction](https://test.whatsonchain.com/tx/f28175616b6dd0ebe2aad41505aabb5bf2864e2e6d1157168183f51b6194d3e6):
|
111 |
+
|
112 |
+

|
113 |
+
|
114 |
+
Congrats! You have deployed and called your first Bitcoin smart contract.
|
115 |
+
|
116 |
+
|
117 |
+
|
118 |
+
|
119 |
+
|
120 |
+
|
121 |
+
|
122 |
+
|
123 |
+
|
how-to-add-a-provider.md
ADDED
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 1
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Add a Provider
|
6 |
+
|
7 |
+
|
8 |
+
In the [contract testing section](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#provider), we learned about the Provider class in sCrypt. This class serves as an abstraction of a Bitcoin node, allowing your application to communicate with the Bitcoin network.
|
9 |
+
|
10 |
+
`sCrypt` provides the following providers by default:
|
11 |
+
|
12 |
+
* `DummyProvider`: A mockup provider intended for local testing. It does not connect to the Bitcoin blockchain and thus cannot send transactions.
|
13 |
+
|
14 |
+
* `DefaultProvider`: The default provider is the safest and easiest way to begin developing on Bitcoin, and is also robust enough for use in production. It can be used in testnet as well as mainnet.
|
15 |
+
|
16 |
+
* For a full list of providers, see [here](../reference/classes/Provider.md#hierarchy).
|
17 |
+
|
18 |
+
## Implementation
|
19 |
+
|
20 |
+
### Base Class `Provider`
|
21 |
+
|
22 |
+
To implement your own provider, you must extend the base class `Provider`. Here's the definition of this class:
|
23 |
+
|
24 |
+
```ts
|
25 |
+
/**
|
26 |
+
* A Provider is an abstraction of non-account-based operations on a blockchain and is generally not directly involved in signing transaction or data.
|
27 |
+
*/
|
28 |
+
export abstract class Provider extends EventEmitter {
|
29 |
+
|
30 |
+
constructor() {
|
31 |
+
super()
|
32 |
+
this._isProvider = true;
|
33 |
+
}
|
34 |
+
|
35 |
+
/**
|
36 |
+
* check if provider is ready
|
37 |
+
*/
|
38 |
+
abstract isConnected(): boolean;
|
39 |
+
|
40 |
+
/**
|
41 |
+
* Implement the connection provider, for example, verify the api key during the connection process.
|
42 |
+
* @returns a connected provider. Throw an exception if the connection fails.
|
43 |
+
*/
|
44 |
+
abstract connect(): Promise<this>;
|
45 |
+
|
46 |
+
/**
|
47 |
+
* update provider network
|
48 |
+
* @param network Network type to be updated
|
49 |
+
*/
|
50 |
+
abstract updateNetwork(network: bsv.Networks.Network): Promise<boolean>;
|
51 |
+
|
52 |
+
/**
|
53 |
+
* @returns The network this provider is connected to.
|
54 |
+
*/
|
55 |
+
abstract getNetwork(): Promise<bsv.Networks.Network>;
|
56 |
+
|
57 |
+
/**
|
58 |
+
* @returns The fee rate for sending transactions through this provider.
|
59 |
+
*/
|
60 |
+
abstract getFeePerKb(): Promise<number>;
|
61 |
+
|
62 |
+
/**
|
63 |
+
* Get a best guess of the fee for a transaction.
|
64 |
+
* @param tx A transaction object to estimate.
|
65 |
+
* @returns The estimated fee in satoshis.
|
66 |
+
*/
|
67 |
+
async getEstimateFee(tx: bsv.Transaction): Promise<number> {
|
68 |
+
const copy = new bsv.Transaction(tx.uncheckedSerialize());
|
69 |
+
// use a copy bcoz `feePerKb` resets all the signatures for inputs.
|
70 |
+
copy.feePerKb(await this.getFeePerKb());
|
71 |
+
return copy.getEstimateFee();
|
72 |
+
}
|
73 |
+
|
74 |
+
// Executions
|
75 |
+
|
76 |
+
/**
|
77 |
+
* Send a raw transaction hex string.
|
78 |
+
* @param rawTxHex The raw transaction hex string to send.
|
79 |
+
* @returns A promise which resolves to the hash of the transaction that has been sent.
|
80 |
+
*/
|
81 |
+
abstract sendRawTransaction(rawTxHex: string): Promise<TxHash>;
|
82 |
+
|
83 |
+
/**
|
84 |
+
* Send a transaction object.
|
85 |
+
* @param tx The transaction object to send.
|
86 |
+
* @returns A promise which resolves to the hash of the transaction that has been sent.
|
87 |
+
* @throws If there is a problem with the `tx` object during serialization.
|
88 |
+
*/
|
89 |
+
sendTransaction(tx: bsv.Transaction): Promise<TxHash> {
|
90 |
+
// TODO: fix tx.serialize issue
|
91 |
+
return this.sendRawTransaction(tx.serialize({ disableIsFullySigned: true }));
|
92 |
+
}
|
93 |
+
|
94 |
+
// Queries
|
95 |
+
|
96 |
+
/**
|
97 |
+
* Get a transaction from the network.
|
98 |
+
* @param txHash The hash value of the transaction.
|
99 |
+
* @returns The query result with the transaction information.
|
100 |
+
*/
|
101 |
+
abstract getTransaction(txHash: TxHash): Promise<TransactionResponse>
|
102 |
+
|
103 |
+
/**
|
104 |
+
* Get a list of the P2PKH UTXOs.
|
105 |
+
* @param address The address of the returned UTXOs belongs to.
|
106 |
+
* @param options The optional query conditions, see details in `UtxoQueryOptions`.
|
107 |
+
* @returns A promise which resolves to a list of UTXO for the query options.
|
108 |
+
*/
|
109 |
+
abstract listUnspent(address: AddressOption, options?: UtxoQueryOptions): Promise<UTXO[]>;
|
110 |
+
|
111 |
+
/**
|
112 |
+
* Get the balance of BSVs in satoshis for an address.
|
113 |
+
* @param address The query address.
|
114 |
+
* @returns A promise which resolves to the address balance status.
|
115 |
+
*/
|
116 |
+
abstract getBalance(address: AddressOption): Promise<{ confirmed: number, unconfirmed: number }>;
|
117 |
+
|
118 |
+
/**
|
119 |
+
* Get a list of UTXO for a certain contract instance.
|
120 |
+
* @param genesisTxHash The hash value of deployment transaction of the contract instance.
|
121 |
+
* @param outputIndex The output index of the deployment transaction of the contract instance.
|
122 |
+
* @returns A promise which resolves to a list of transaction UTXO.
|
123 |
+
*/
|
124 |
+
abstract getContractUTXOs(genesisTxHash: TxHash, outputIndex: number): Promise<UTXO[]>;
|
125 |
+
|
126 |
+
// Inspection
|
127 |
+
|
128 |
+
readonly _isProvider: boolean;
|
129 |
+
|
130 |
+
/**
|
131 |
+
* Check if an object is a `Provider`
|
132 |
+
* @param value The target object
|
133 |
+
* @returns Returns `true` if and only if `object` is a Provider.
|
134 |
+
*/
|
135 |
+
static isProvider(value: any): value is Provider {
|
136 |
+
return !!(value && value._isProvider);
|
137 |
+
}
|
138 |
+
}
|
139 |
+
```
|
140 |
+
|
141 |
+
It is recommended that your provider implements all `abstract` methods. For non-`abstract` methods, the default implementation is usually sufficient.
|
142 |
+
|
143 |
+
|
144 |
+
### `Example: WhatsonchainProvider`
|
145 |
+
|
146 |
+
Let's walk through the process of implementing our own provider. In this example we'll implement a provider for [WhatsOnChain](https://whatsonchain.com) (WoC).
|
147 |
+
|
148 |
+
|
149 |
+
1. First let's implement the `isConnected()` and `connect()` functions. Because WoC doesn't need to maintan an open connection, not does it require any authentication by default, it's simply marked as connected by default. If your chosen provider does, here's probably the place to implement the connection logic.
|
150 |
+
|
151 |
+
```ts
|
152 |
+
isConnected(): boolean {
|
153 |
+
return true;
|
154 |
+
}
|
155 |
+
|
156 |
+
override async connect(): Promise<this> {
|
157 |
+
this.emit(ProviderEvent.Connected, true);
|
158 |
+
return Promise.resolve(this);
|
159 |
+
}
|
160 |
+
```
|
161 |
+
|
162 |
+
2. Next, we'll implement the network functions. Here, your providers selected network can be toggled. WoC supports both the Bitcoin mainnet along with testnet, so we don't do further checking:
|
163 |
+
|
164 |
+
```ts
|
165 |
+
override async updateNetwork(network: bsv.Networks.Network): Promise<boolean> {
|
166 |
+
this._network = network;
|
167 |
+
this.emit(ProviderEvent.NetworkChange, network);
|
168 |
+
return Promise.resolve(true);
|
169 |
+
}
|
170 |
+
|
171 |
+
override async getNetwork(): Promise<bsv.Networks.Network> {
|
172 |
+
return Promise.resolve(this._network);
|
173 |
+
}
|
174 |
+
```
|
175 |
+
|
176 |
+
If your provider is only meant for the testnet, you could do something like this:
|
177 |
+
```ts
|
178 |
+
override async updateNetwork(network: bsv.Networks.Network): Promise<boolean> {
|
179 |
+
if (network != bsv.Networks.testnet) {
|
180 |
+
throw new Error('Network not supported.')
|
181 |
+
}
|
182 |
+
this._network = network;
|
183 |
+
this.emit(ProviderEvent.NetworkChange, network);
|
184 |
+
return Promise.resolve(true);
|
185 |
+
}
|
186 |
+
```
|
187 |
+
|
188 |
+
3. Now let's set the transaction fee rate. In our example, we hard-code the value to be 50 satoshis per Kb:
|
189 |
+
|
190 |
+
```ts
|
191 |
+
override async getFeePerKb(): Promise<number> {
|
192 |
+
return Promise.resolve(50);
|
193 |
+
}
|
194 |
+
```
|
195 |
+
|
196 |
+
4. Let's implement the function that will send the transaction data to our provider:
|
197 |
+
|
198 |
+
```ts
|
199 |
+
override async sendRawTransaction(rawTxHex: string): Promise<TxHash> {
|
200 |
+
// 1 second per KB
|
201 |
+
const size = Math.max(1, rawTxHex.length / 2 / 1024); //KB
|
202 |
+
const timeout = Math.max(10000, 1000 * size);
|
203 |
+
try {
|
204 |
+
const res = await superagent.post(
|
205 |
+
`${this.apiPrefix}/tx/raw`
|
206 |
+
)
|
207 |
+
.timeout({
|
208 |
+
response: timeout, // Wait 5 seconds for the server to start sending,
|
209 |
+
deadline: 60000, // but allow 1 minute for the file to finish loading.
|
210 |
+
})
|
211 |
+
.set('Content-Type', 'application/json')
|
212 |
+
.send({ txhex: rawTxHex })
|
213 |
+
return res.body;
|
214 |
+
} catch (error) {
|
215 |
+
if (error.response && error.response.text) {
|
216 |
+
throw new Error(`WhatsonchainProvider ERROR: ${error.response.text}`)
|
217 |
+
}
|
218 |
+
throw new Error(`WhatsonchainProvider ERROR: ${error.message}`)
|
219 |
+
}
|
220 |
+
}
|
221 |
+
```
|
222 |
+
|
223 |
+
In the function we use the [`superagent`](https://www.npmjs.com/package/superagent) to send requests to WoC's HTTP endpoint. Check out their [docs](https://docs.taal.com/core-products/whatsonchain) for a description of the endpoints they provide.
|
224 |
+
|
225 |
+
5. Now we need to implement some queries. First let's implement the function to get a list of [UTXO's](https://wiki.bitcoinsv.io/index.php/UTXO) for a certain address:
|
226 |
+
|
227 |
+
```ts
|
228 |
+
override async listUnspent(
|
229 |
+
address: AddressOption,
|
230 |
+
options?: UtxoQueryOptions
|
231 |
+
): Promise<UTXO[]> {
|
232 |
+
|
233 |
+
const res = await superagent.get(`${this.apiPrefix}/address/${address}/unspent`);
|
234 |
+
const utxos: UTXO[] =
|
235 |
+
res.body.map(item => ({
|
236 |
+
txId: item.tx_hash,
|
237 |
+
outputIndex: item.tx_pos,
|
238 |
+
satoshis: item.value,
|
239 |
+
script: bsv.Script.buildPublicKeyHashOut(address).toHex(),
|
240 |
+
}));
|
241 |
+
|
242 |
+
if (options?.minSatoshis && utxos.reduce((s, u) => s + u.satoshis, 0) < options.minSatoshis) {
|
243 |
+
throw new Error(`WhatsonchainProvider ERROR: not enough utxos for the request amount of ${options.minSatoshis} on address ${address.toString()}`);
|
244 |
+
}
|
245 |
+
|
246 |
+
return utxos;
|
247 |
+
}
|
248 |
+
```
|
249 |
+
|
250 |
+
Next, we'll make the `getBalance` function parse out the addresses balance from the UTXO's:
|
251 |
+
|
252 |
+
```ts
|
253 |
+
override async getBalance(
|
254 |
+
address?: AddressOption
|
255 |
+
): Promise<{ confirmed: number, unconfirmed: number }> {
|
256 |
+
|
257 |
+
return this.listUnspent(address, { minSatoshis: 0 }).then(utxos => {
|
258 |
+
return {
|
259 |
+
confirmed: utxos.reduce((acc, utxo) => {
|
260 |
+
acc += utxo.satoshis;
|
261 |
+
return acc;
|
262 |
+
}, 0),
|
263 |
+
unconfirmed: 0
|
264 |
+
}
|
265 |
+
})
|
266 |
+
|
267 |
+
}
|
268 |
+
```
|
269 |
+
|
270 |
+
We also implement the function to query the raw transaction using the transactions ID:
|
271 |
+
```ts
|
272 |
+
override async getTransaction(txHash: string): Promise<TransactionResponse> {
|
273 |
+
try {
|
274 |
+
const res = await superagent.get(`${this.apiPrefix}/tx/${txHash}/hex`);
|
275 |
+
return new bsv.Transaction(res.text)
|
276 |
+
} catch (e) {
|
277 |
+
throw new Error(`WhatsonchainProvider ERROR: failed fetching raw transaction data: ${e.message}`);
|
278 |
+
}
|
279 |
+
}
|
280 |
+
```
|
281 |
+
|
282 |
+
|
283 |
+
Lastly, if our provider doesn't support a certain query, we can simply throw an error by default:
|
284 |
+
```ts
|
285 |
+
override async getContractUTXOs(genesisTxHash: string, outputIndex?: number): Promise<UTXO[]> {
|
286 |
+
throw new Error("Method #getContractUTXOs not implemented in WhatsonchainProvider.");
|
287 |
+
}
|
288 |
+
```
|
289 |
+
|
290 |
+
## Using the Provider
|
291 |
+
|
292 |
+
Providers are usually used by a `Signer`:
|
293 |
+
|
294 |
+
```ts
|
295 |
+
const provider = new WhatsonchainProvider(bsv.Networks.mainnet)
|
296 |
+
const signer = new TestWallet(privateKey, provider)
|
297 |
+
|
298 |
+
await contractInstance.connect(signer);
|
299 |
+
```
|
300 |
+
|
301 |
+
Here, the signer will use our `WhatsonchainProvider` for each Bitcoin network operation it needs. The next section describes signers and how we can implement a custom one.
|
how-to-add-a-signer.md
ADDED
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 2
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Add a Signer
|
6 |
+
|
7 |
+
|
8 |
+
As described in [this section](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#provider), a signer is an abstraction of private keys, which can be used to sign messages and transactions. A simple signer would be a single private key, while a complex signer is a wallet.
|
9 |
+
|
10 |
+
`sCrypt` provides the following signers by default:
|
11 |
+
|
12 |
+
1. `TestWallet` : a simple wallet that can hold multiple private keys, with in-memory utxo management. Should only be used for testing.
|
13 |
+
2. `SensiletSigner`: a signer powered by the popular smart contract wallet [Sensilet](https://sensilet.com/). Can be used in production.
|
14 |
+
|
15 |
+
## Implementation
|
16 |
+
|
17 |
+
### Base Class `Signer`
|
18 |
+
|
19 |
+
If you want to implement your own signer, you must inherit from the base class `Signer`.
|
20 |
+
|
21 |
+
|
22 |
+
```ts
|
23 |
+
/**
|
24 |
+
* A `Signer` is a class which in some way directly or indirectly has access to a private key, which can sign messages and transactions to authorize the network to perform operations.
|
25 |
+
*/
|
26 |
+
export abstract class Signer {
|
27 |
+
|
28 |
+
provider?: Provider;
|
29 |
+
readonly _isSigner: boolean;
|
30 |
+
|
31 |
+
constructor(provider?: Provider) {
|
32 |
+
this._isSigner = true;
|
33 |
+
this.provider = provider;
|
34 |
+
}
|
35 |
+
|
36 |
+
/**
|
37 |
+
* Connect a provider to `this`.
|
38 |
+
* @param provider The target provider.
|
39 |
+
* @returns
|
40 |
+
*/
|
41 |
+
abstract connect(provider: Provider): Promise<this>;
|
42 |
+
|
43 |
+
// Account
|
44 |
+
|
45 |
+
/**
|
46 |
+
*
|
47 |
+
* @returns A promise which resolves to the address to the default private key of the signer.
|
48 |
+
*/
|
49 |
+
abstract getDefaultAddress(): Promise<bsv.Address>;
|
50 |
+
|
51 |
+
/**
|
52 |
+
*
|
53 |
+
* @returns A promise which resolves to the public key of the default private key of the signer.
|
54 |
+
*/
|
55 |
+
abstract getDefaultPubKey(): Promise<bsv.PublicKey>;
|
56 |
+
|
57 |
+
/**
|
58 |
+
*
|
59 |
+
* @param address The request address, using the default address if omitted.
|
60 |
+
* @returns The public key result.
|
61 |
+
* @throws If the private key for the address does not belong this signer.
|
62 |
+
*/
|
63 |
+
abstract getPubKey(address?: AddressOption): Promise<bsv.PublicKey>;
|
64 |
+
|
65 |
+
// Signing
|
66 |
+
|
67 |
+
/**
|
68 |
+
* Sign a raw transaction hex string.
|
69 |
+
*
|
70 |
+
* @param rawTxHex The raw transaction hex to sign.
|
71 |
+
* @param options The options for signing, see the details of `SignTransactionOptions`.
|
72 |
+
* @returns A promise which resolves to the signed transaction hex string.
|
73 |
+
* @throws If any input of the transaction can not be signed properly.
|
74 |
+
*/
|
75 |
+
abstract signRawTransaction(rawTxHex: string, options: SignTransactionOptions): Promise<string>;
|
76 |
+
|
77 |
+
/**
|
78 |
+
* Sign a transaction object.
|
79 |
+
* @param tx The transaction object to sign.
|
80 |
+
* @param options The options for signing, see the details of `SignTransactionOptions`.
|
81 |
+
* @returns A promise which resolves to the signed transaction object.
|
82 |
+
*/
|
83 |
+
abstract signTransaction(tx: bsv.Transaction, options?: SignTransactionOptions): Promise<bsv.Transaction>;
|
84 |
+
|
85 |
+
/**
|
86 |
+
* Sign a message string.
|
87 |
+
* @param message The message to be signed.
|
88 |
+
* @param address The optional address whose private key will be used to sign `message`, using the default private key if omitted.
|
89 |
+
* @returns A promise which resolves to the signautre of the message.
|
90 |
+
*/
|
91 |
+
abstract signMessage(message: string, address?: AddressOption): Promise<string>;
|
92 |
+
|
93 |
+
/**
|
94 |
+
* Get the requested transaction signatures for the raw transaction.
|
95 |
+
* @param rawTxHex The raw transaction hex to get signatures from.
|
96 |
+
* @param sigRequests The signature requst informations, see details in `SignatureRequest`.
|
97 |
+
* @returns A promise which resolves to a list of `SignatureReponse` corresponding to `sigRequests`.
|
98 |
+
*/
|
99 |
+
abstract getSignatures(rawTxHex: string, sigRequests: SignatureRequest[]): Promise<SignatureResponse[]>;
|
100 |
+
|
101 |
+
/**
|
102 |
+
* Get the connected provider.
|
103 |
+
* @returns the connected provider.
|
104 |
+
* @throws if no provider is connected to `this`.
|
105 |
+
*/
|
106 |
+
get connectedProvider(): Provider {
|
107 |
+
if (!this.provider) {
|
108 |
+
throw new Error(`the provider of singer ${this.constructor.name} is not set yet!`);
|
109 |
+
}
|
110 |
+
if (!this.provider.isConnected()) {
|
111 |
+
throw new Error(`the provider of singer ${this.constructor.name} is not connected yet!`);
|
112 |
+
}
|
113 |
+
|
114 |
+
return this.provider;
|
115 |
+
}
|
116 |
+
|
117 |
+
/**
|
118 |
+
* Sign the transaction, then broadcast the transaction
|
119 |
+
* @param tx A transaction is signed and broadcast
|
120 |
+
* @param options The options for signing, see the details of `SignTransactionOptions`.
|
121 |
+
* @returns A promise which resolves to the transaction id.
|
122 |
+
*/
|
123 |
+
async signAndsendTransaction(tx: bsv.Transaction, options?: SignTransactionOptions): Promise<TransactionResponse> {
|
124 |
+
await tx.sealAsync();
|
125 |
+
const signedTx = await this.signTransaction(tx, options);
|
126 |
+
await this.connectedProvider.sendTransaction(signedTx);
|
127 |
+
return signedTx;
|
128 |
+
};
|
129 |
+
|
130 |
+
/**
|
131 |
+
* Get a list of the P2PKH UTXOs.
|
132 |
+
* @param address The address of the returned UTXOs belongs to.
|
133 |
+
* @param options The optional query conditions, see details in `UtxoQueryOptions`.
|
134 |
+
* @returns A promise which resolves to a list of UTXO for the query options.
|
135 |
+
*/
|
136 |
+
listUnspent(address: AddressOption, options?: UtxoQueryOptions): Promise<UTXO[]> {
|
137 |
+
// default implemention using provider, can be overrided.
|
138 |
+
return this.connectedProvider.listUnspent(address, options);
|
139 |
+
}
|
140 |
+
|
141 |
+
/**
|
142 |
+
* Get the balance of BSVs in satoshis for an address.
|
143 |
+
* @param address The query address.
|
144 |
+
* @returns A promise which resolves to the address balance status.
|
145 |
+
*/
|
146 |
+
getBalance(address?: AddressOption): Promise<{ confirmed: number, unconfirmed: number }> {
|
147 |
+
// default implemention using provider, can be overrided.
|
148 |
+
return this.connectedProvider.getBalance(address);
|
149 |
+
}
|
150 |
+
|
151 |
+
// Inspection
|
152 |
+
/**
|
153 |
+
* Check if an object is a `Signer`
|
154 |
+
* @param value The target object
|
155 |
+
* @returns Returns `true` if and only if `object` is a Provider.
|
156 |
+
*/
|
157 |
+
static isSigner(value: any): value is Signer {
|
158 |
+
return !!(value && value._isSigner);
|
159 |
+
}
|
160 |
+
|
161 |
+
}
|
162 |
+
```
|
163 |
+
|
164 |
+
It is recommended that your signer implements all `abstract` methods. For non-`abstract` methods, the default implementation is usually sufficient.
|
165 |
+
|
166 |
+
|
167 |
+
### `Example: SensiletSigner`
|
168 |
+
|
169 |
+
Next, we use the [Sensilet wallet](https://sensilet.com/) as an example to show how to implement a `SensiletSigner`.
|
170 |
+
|
171 |
+
|
172 |
+
1. In the `connect` method, you usually attempt to connect to a provider and save it:
|
173 |
+
|
174 |
+
```ts
|
175 |
+
override async connect(provider: Provider): Promise<this> {
|
176 |
+
// we should make sure sensilet is connected before we connect a provider.
|
177 |
+
const isSensiletConnected = await this.isSensiletConnected();
|
178 |
+
|
179 |
+
if(!isSensiletConnected) {
|
180 |
+
Promise.reject(new Error('Sensilet is not connected!'))
|
181 |
+
}
|
182 |
+
|
183 |
+
if(!provider.isConnected()) {
|
184 |
+
// connect the provider
|
185 |
+
await provider.connect();
|
186 |
+
}
|
187 |
+
|
188 |
+
this.provider = provider;
|
189 |
+
return this;
|
190 |
+
}
|
191 |
+
```
|
192 |
+
|
193 |
+
2. Returns the address to the default private key of the wallet in `getDefaultAddress`:
|
194 |
+
|
195 |
+
```ts
|
196 |
+
/**
|
197 |
+
* Get an object that can directly interact with the Sensilet wallet,
|
198 |
+
* if there is no connection with the wallet, it will request to establish a connection.
|
199 |
+
* @returns SensiletWalletAPI
|
200 |
+
*/
|
201 |
+
async getConnectedTarget(): Promise<SensiletWalletAPI> {
|
202 |
+
|
203 |
+
const isSensiletConnected = await this.isSensiletConnected();
|
204 |
+
if (!isSensiletConnected) {
|
205 |
+
// trigger connecting to sensilet account when it's not connected.
|
206 |
+
try {
|
207 |
+
const addr = await this._target.requestAccount();
|
208 |
+
this._address = bsv.Address.fromString(addr);
|
209 |
+
} catch (e) {
|
210 |
+
throw new Error('Sensilet requestAccount failed')
|
211 |
+
}
|
212 |
+
}
|
213 |
+
return this.getSensilet();
|
214 |
+
}
|
215 |
+
|
216 |
+
override async getDefaultAddress(): Promise<bsv.Address> {
|
217 |
+
//
|
218 |
+
const sensilet = await this.getConnectedTarget();
|
219 |
+
const address = await sensilet.getAddress();
|
220 |
+
return bsv.Address.fromString(address);
|
221 |
+
}
|
222 |
+
```
|
223 |
+
|
224 |
+
3. Returns the public key to the default private key of the wallet in `getDefaultPubKey`:
|
225 |
+
|
226 |
+
```ts
|
227 |
+
override async getDefaultPubKey(): Promise<PublicKey> {
|
228 |
+
const sensilet = await this.getConnectedTarget();
|
229 |
+
const pubKey = await sensilet.getPublicKey();
|
230 |
+
return Promise.resolve(new bsv.PublicKey(pubKey));
|
231 |
+
}
|
232 |
+
```
|
233 |
+
|
234 |
+
4. Since Sensilet is a single-address wallet, we simply ignore the `getPubKey` method:
|
235 |
+
|
236 |
+
```ts
|
237 |
+
override async getPubKey(address: AddressOption): Promise<PublicKey> {
|
238 |
+
throw new Error(`Method ${this.constructor.name}#getPubKey not implemented.`);
|
239 |
+
}
|
240 |
+
```
|
241 |
+
|
242 |
+
5. Both `signTransaction` and `signRawTransaction` sign the transaction, but their parameters are different. `signRawTransaction` converts the parameters and delegates the implementation of the signing to `signTransaction`.
|
243 |
+
|
244 |
+
The following are types used in these two functions:
|
245 |
+
|
246 |
+
|
247 |
+
```ts
|
248 |
+
|
249 |
+
/**
|
250 |
+
* `SignatureRequest` contains required informations for a signer to sign a certain input of a transaction.
|
251 |
+
*/
|
252 |
+
export interface SignatureRequest {
|
253 |
+
/** The index of input to sign. */
|
254 |
+
inputIndex: number;
|
255 |
+
/** The previous output satoshis value of the input to spend. */
|
256 |
+
satoshis: number;
|
257 |
+
/** The address(es) of corresponding private key(s) required to sign the input. */
|
258 |
+
address: AddressesOption;
|
259 |
+
/** The previous output script of input, default value is a P2PKH locking script for the `address` if omitted. */
|
260 |
+
scriptHex?: string;
|
261 |
+
/** The sighash type, default value is `SIGHASH_ALL | SIGHASH_FORKID` if omitted. */
|
262 |
+
sigHashType?: number;
|
263 |
+
/** The extra information for signing. */
|
264 |
+
data?: unknown;
|
265 |
+
}
|
266 |
+
|
267 |
+
/**
|
268 |
+
* `SignatureResponse` contains the signing result corresponding to a `SignatureRequest`.
|
269 |
+
*/
|
270 |
+
export interface SignatureResponse {
|
271 |
+
/** The index of input. */
|
272 |
+
inputIndex: number;
|
273 |
+
/** The signature.*/
|
274 |
+
sig: string;
|
275 |
+
/** The public key bound with the `sig`. */
|
276 |
+
publicKey: string;
|
277 |
+
/** The sighash type, default value is `SIGHASH_ALL | SIGHASH_FORKID` if omitted. */
|
278 |
+
sigHashType: number;
|
279 |
+
}
|
280 |
+
|
281 |
+
/**
|
282 |
+
* `SignTransactionOptions` is the options can be provided when signing a transaction.
|
283 |
+
*/
|
284 |
+
export interface SignTransactionOptions {
|
285 |
+
/** The `SignatureRequest` for the some inputs of the transaction. */
|
286 |
+
sigRequests?: SignatureRequest[];
|
287 |
+
/** The address(es) whose corresponding private key(s) should be used to sign the tx. */
|
288 |
+
address?: AddressesOption;
|
289 |
+
}
|
290 |
+
```
|
291 |
+
|
292 |
+
`signTransaction` will convert the above parameter types to the parameter types required by the [sensilet api](https://doc.sensilet.com/guide/sensilet-api.html#signtx). And call the sensilet api to complete the signature, which is implemented in `getSignatures` function.
|
293 |
+
|
294 |
+
```ts
|
295 |
+
override async signRawTransaction(rawTxHex: string, options: SignTransactionOptions): Promise<string> {
|
296 |
+
// convert `rawTxHex` to a transation object
|
297 |
+
const sigReqsByInputIndex: Map<number, SignatureRequest> = (options?.sigRequests || []).reduce((m, sigReq) => { m.set(sigReq.inputIndex, sigReq); return m; }, new Map());
|
298 |
+
const tx = new bsv.Transaction(rawTxHex);
|
299 |
+
tx.inputs.forEach((_, inputIndex) => {
|
300 |
+
const sigReq = sigReqsByInputIndex.get(inputIndex);
|
301 |
+
if (!sigReq) {
|
302 |
+
throw new Error(`\`SignatureRequest\` info should be provided for the input ${inputIndex} to call #signRawTransaction`)
|
303 |
+
}
|
304 |
+
const script = sigReq.scriptHex ? new bsv.Script(sigReq.scriptHex) : bsv.Script.buildPublicKeyHashOut(sigReq.address.toString());
|
305 |
+
// set ref output of the input
|
306 |
+
tx.inputs[inputIndex].output = new bsv.Transaction.Output({
|
307 |
+
script,
|
308 |
+
satoshis: sigReq.satoshis
|
309 |
+
})
|
310 |
+
});
|
311 |
+
|
312 |
+
const signedTx = await this.signTransaction(tx, options);
|
313 |
+
return signedTx.toString();
|
314 |
+
}
|
315 |
+
|
316 |
+
override async signTransaction(tx: Transaction, options?: SignTransactionOptions): Promise<Transaction> {
|
317 |
+
|
318 |
+
const network = await this.getNetwork();
|
319 |
+
// Generate default `sigRequests` if not passed by user
|
320 |
+
const sigRequests: SignatureRequest[] = options?.sigRequests?.length ? options.sigRequests :
|
321 |
+
|
322 |
+
tx.inputs.map((input, inputIndex) => {
|
323 |
+
const useAddressToSign = options && options.address ? options.address :
|
324 |
+
input.output?.script.isPublicKeyHashOut()
|
325 |
+
? input.output.script.toAddress(network)
|
326 |
+
: this._address;
|
327 |
+
|
328 |
+
return {
|
329 |
+
inputIndex,
|
330 |
+
satoshis: input.output?.satoshis,
|
331 |
+
address: useAddressToSign,
|
332 |
+
scriptHex: input.output?.script?.toHex(),
|
333 |
+
sigHashType: DEFAULT_SIGHASH_TYPE,
|
334 |
+
}
|
335 |
+
})
|
336 |
+
|
337 |
+
const sigResponses = await this.getSignatures(tx.toString(), sigRequests);
|
338 |
+
|
339 |
+
// Set the acquired signature as an unlocking script for the transaction
|
340 |
+
tx.inputs.forEach((input, inputIndex) => {
|
341 |
+
const sigResp = sigResponses.find(sigResp => sigResp.inputIndex === inputIndex);
|
342 |
+
if (sigResp && input.output?.script.isPublicKeyHashOut()) {
|
343 |
+
var unlockingScript = new bsv.Script("")
|
344 |
+
.add(Buffer.from(sigResp.sig, 'hex'))
|
345 |
+
.add(Buffer.from(sigResp.publicKey, 'hex'));
|
346 |
+
|
347 |
+
input.setScript(unlockingScript)
|
348 |
+
}
|
349 |
+
})
|
350 |
+
|
351 |
+
return tx;
|
352 |
+
}
|
353 |
+
|
354 |
+
/**
|
355 |
+
* Get signatures with sensilet api
|
356 |
+
* @param rawTxHex a transation raw hex
|
357 |
+
* @param sigRequests a `SignatureRequest` array for the some inputs of the transaction.
|
358 |
+
* @returns a `SignatureResponse` array
|
359 |
+
*/
|
360 |
+
async getSignatures(rawTxHex: string, sigRequests: SignatureRequest[]): Promise<SignatureResponse[]> {
|
361 |
+
const network = await this.getNetwork()
|
362 |
+
// convert `sigRequests` to the parameter type required by sensilet `signTx` api
|
363 |
+
const inputInfos = sigRequests.flatMap((sigReq) => {
|
364 |
+
const addresses = parseAddresses(sigReq.address, network);
|
365 |
+
return addresses.map(address => {
|
366 |
+
return {
|
367 |
+
txHex: rawTxHex,
|
368 |
+
inputIndex: sigReq.inputIndex,
|
369 |
+
scriptHex: sigReq.scriptHex || bsv.Script.buildPublicKeyHashOut(address).toHex(),
|
370 |
+
satoshis: sigReq.satoshis,
|
371 |
+
sigtype: sigReq.sigHashType || DEFAULT_SIGHASH_TYPE,
|
372 |
+
address: address.toString()
|
373 |
+
}
|
374 |
+
});
|
375 |
+
});
|
376 |
+
|
377 |
+
const sensilet = await this.getConnectedTarget();
|
378 |
+
// call sensilet `signTx` api to sign transaction
|
379 |
+
// https://doc.sensilet.com/guide/sensilet-api.html#signtx
|
380 |
+
const sigResults = await sensilet.signTx({
|
381 |
+
list: inputInfos
|
382 |
+
});
|
383 |
+
|
384 |
+
return inputInfos.map((inputInfo, idx) => {
|
385 |
+
return {
|
386 |
+
inputIndex: inputInfo.inputIndex,
|
387 |
+
sig: sigResults.sigList[idx].sig,
|
388 |
+
publicKey: sigResults.sigList[idx].publicKey,
|
389 |
+
sigHashType: sigRequests[idx].sigHashType || DEFAULT_SIGHASH_TYPE
|
390 |
+
}
|
391 |
+
})
|
392 |
+
}
|
393 |
+
```
|
394 |
+
|
395 |
+
6. Sensilet supports signing messages, if your wallet does not support it, you can throw an exception in the `signMessage` function:
|
396 |
+
|
397 |
+
```ts
|
398 |
+
override async signMessage(message: string, address?: AddressOption): Promise<string> {
|
399 |
+
if (address) {
|
400 |
+
throw new Error(`${this.constructor.name}#signMessge with \`address\` param is not supported!`);
|
401 |
+
}
|
402 |
+
const sensilet = await this.getConnectedTarget();
|
403 |
+
return sensilet.signMessage(message);
|
404 |
+
}
|
405 |
+
```
|
406 |
+
|
407 |
+
So far, we have implemented all abstract methods. The remaining non-abstract methods can reuse the default implementation, that is, delegating to the connected [provider](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#provider). If you have a customized implementation, you can override them. For example, we can use the [Sensilet api `getBsvBalance`](https://doc.sensilet.com/guide/sensilet-api.html#getbsvbalance) to obtain the balance of an address.
|
408 |
+
|
409 |
+
```ts
|
410 |
+
override getBalance(address?: AddressOption): Promise<{ confirmed: number, unconfirmed: number }> {
|
411 |
+
if(address) {
|
412 |
+
return this.connectedProvider.getBalance(address);
|
413 |
+
}
|
414 |
+
return this.getConnectedTarget().then(target => target.getBsvBalance()).then(r => r.balance)
|
415 |
+
}
|
416 |
+
```
|
417 |
+
|
418 |
+
Now we have implemented `SensiletSigner`. The full code is [here](https://gist.github.com/xhliu/73104028deaf95c8b6665bf96496fe11#file-sensiletsigner-ts-L44).
|
419 |
+
|
420 |
+
## Use your signer
|
421 |
+
|
422 |
+
Just connect your signer to a smart contract instance like any other signers:
|
423 |
+
|
424 |
+
```ts
|
425 |
+
// declare your signer
|
426 |
+
const your_signer = new YourSigner(new DefaultProvider());
|
427 |
+
// connect the signer to the contract instance
|
428 |
+
await instance.connect(your_signer);
|
429 |
+
```
|
430 |
+
|
how-to-call-multiple-contracts.md
ADDED
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 3
|
3 |
+
---
|
4 |
+
|
5 |
+
# Call Multiple Contracts in a Single Tx
|
6 |
+
|
7 |
+
Up to now, we have only shown how to call one smart contract in a transaction. That is, only one input of the tx spends a smart contract UTXO, and the other inputs, if any, spend Pay-to-Public-Key-Hash ([P2PKH](https://learnmeabitcoin.com/guide/p2pkh)) UTXOs, which are generally NOT regarded as smart contracts.
|
8 |
+
|
9 |
+
There are cases where it is desirable to spend multiple smart contract UTXOs in different inputs of a tx.
|
10 |
+
|
11 |
+
The main differences from [calling a single contract](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#contract-call) are:
|
12 |
+
|
13 |
+
1. Set `multiContractCall = true` in `MethodCallOptions`
|
14 |
+
2. Each call may only return a partial/incomplete transaction, instead of a complete transaction
|
15 |
+
3. A partial tx has to be passed as `ContractTransaction` in `MethodCallOptions` in subsequent calls
|
16 |
+
4. Finally invoke `SmartContract.multiContractCall(partialContractTx: ContractTransaction, signer: Signer)` to sign and broadcast the complete transaction
|
17 |
+
|
18 |
+
|
19 |
+
The following is an [example code](https://github.com/sCrypt-Inc/boilerplate/blob/master/tests/testnet/multi_contracts_call.ts) of calling two contracts at the same time:
|
20 |
+
|
21 |
+
|
22 |
+
```ts
|
23 |
+
import { Counter } from '../../src/contracts/counter'
|
24 |
+
import { getDefaultSigner } from '../utils/helper'
|
25 |
+
import { HashPuzzle } from '../../src/contracts/hashPuzzle'
|
26 |
+
|
27 |
+
async function main() {
|
28 |
+
await Counter.compile()
|
29 |
+
await HashPuzzle.compile()
|
30 |
+
|
31 |
+
const signer = getDefaultSigner()
|
32 |
+
let counter = new Counter(1n)
|
33 |
+
|
34 |
+
// connect to a signer
|
35 |
+
await counter.connect(signer)
|
36 |
+
|
37 |
+
// contract deployment
|
38 |
+
const deployTx = await counter.deploy(1)
|
39 |
+
console.log('Counter contract deployed: ', deployTx.id)
|
40 |
+
|
41 |
+
counter.bindTxBuilder(
|
42 |
+
'incrementOnChain',
|
43 |
+
(
|
44 |
+
current: Counter,
|
45 |
+
options: MethodCallOptions<Counter>,
|
46 |
+
...args: any
|
47 |
+
): Promise<ContractTransaction> => {
|
48 |
+
// create the next instance from the current
|
49 |
+
const nextInstance = current.next()
|
50 |
+
// apply updates on the next instance locally
|
51 |
+
nextInstance.count++
|
52 |
+
|
53 |
+
const tx = new bsv.Transaction()
|
54 |
+
tx.addInput(current.buildContractInput(options.fromUTXO)).addOutput(
|
55 |
+
new bsv.Transaction.Output({
|
56 |
+
script: nextInstance.lockingScript,
|
57 |
+
satoshis: current.balance,
|
58 |
+
})
|
59 |
+
)
|
60 |
+
|
61 |
+
return Promise.resolve({
|
62 |
+
tx: tx,
|
63 |
+
atInputIndex: 0,
|
64 |
+
nexts: [
|
65 |
+
{
|
66 |
+
instance: nextInstance,
|
67 |
+
balance: current.balance,
|
68 |
+
atOutputIndex: 0,
|
69 |
+
},
|
70 |
+
],
|
71 |
+
})
|
72 |
+
}
|
73 |
+
)
|
74 |
+
|
75 |
+
const plainText = 'abc'
|
76 |
+
const byteString = toByteString(plainText, true)
|
77 |
+
const sha256Data = sha256(byteString)
|
78 |
+
|
79 |
+
const hashPuzzle = new HashPuzzle(sha256Data)
|
80 |
+
|
81 |
+
// connect to a signer
|
82 |
+
await hashPuzzle.connect(signer)
|
83 |
+
|
84 |
+
const deployTx1 = await hashPuzzle.deploy(1)
|
85 |
+
console.log('HashPuzzle contract deployed: ', deployTx1.id)
|
86 |
+
|
87 |
+
hashPuzzle.bindTxBuilder(
|
88 |
+
'unlock',
|
89 |
+
(
|
90 |
+
current: HashPuzzle,
|
91 |
+
options: MethodCallOptions<HashPuzzle>,
|
92 |
+
...args: any
|
93 |
+
): Promise<ContractTransaction> => {
|
94 |
+
if (options.partialContractTx) {
|
95 |
+
const unSignedTx = options.partialContractTx.tx
|
96 |
+
unSignedTx.addInput(
|
97 |
+
current.buildContractInput(options.fromUTXO)
|
98 |
+
)
|
99 |
+
|
100 |
+
return Promise.resolve({
|
101 |
+
tx: unSignedTx,
|
102 |
+
atInputIndex: 1,
|
103 |
+
nexts: [],
|
104 |
+
})
|
105 |
+
}
|
106 |
+
|
107 |
+
throw new Error('no partialContractTx found')
|
108 |
+
}
|
109 |
+
)
|
110 |
+
|
111 |
+
const partialTx = await counter.methods.incrementOnChain({
|
112 |
+
multiContractCall: true,
|
113 |
+
} as MethodCallOptions<Counter>)
|
114 |
+
|
115 |
+
const finalTx = await hashPuzzle.methods.unlock(
|
116 |
+
byteString,
|
117 |
+
{
|
118 |
+
multiContractCall: true,
|
119 |
+
partialContractTx: partialTx,
|
120 |
+
} as MethodCallOptions<HashPuzzle>
|
121 |
+
)
|
122 |
+
|
123 |
+
const { tx: callTx, nexts } = await SmartContract.multiContractCall(
|
124 |
+
finalTx,
|
125 |
+
signer
|
126 |
+
)
|
127 |
+
|
128 |
+
console.log('Counter, HashPuzzle contract `unlock` called: ', callTx.id)
|
129 |
+
|
130 |
+
// hashPuzzle has terminated, but counter can still be called
|
131 |
+
counter = nexts[0].instance
|
132 |
+
}
|
133 |
+
|
134 |
+
await main()
|
135 |
+
|
136 |
+
```
|
137 |
+
|
138 |
+
|
139 |
+
|
140 |
+
:::note
|
141 |
+
- You must bind a [transition builder](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#tx-builders) to each contract instance, since [the default](../how-to-deploy-and-call-a-contract/how-to-customize-a-contract-tx.md#customize-1) only spends a single contract UTXO.
|
142 |
+
- If the called contracts need signatures from different private keys to be called, the signer passed to `multiContractCall` must have all private keys.
|
143 |
+
:::
|
144 |
+
|
how-to-customize-a-contract-tx.md
ADDED
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 2
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Customize a Contract Tx
|
6 |
+
|
7 |
+
|
8 |
+
## Deployment Tx
|
9 |
+
|
10 |
+
### Default
|
11 |
+
For contract deployment, the default tx builder creates a transaction with the following structure:
|
12 |
+
|
13 |
+
* Inputs:
|
14 |
+
|
15 |
+
* [0β¦]: One or more [P2PKH](https://learnmeabitcoin.com/technical/p2pkh) inputs for paying transaction fees.
|
16 |
+
|
17 |
+
* Outputs:
|
18 |
+
|
19 |
+
* [0]: The output containing the contract.
|
20 |
+
* [1]: A P2PKH change output if needed.
|
21 |
+
|
22 |
+
Numbers in [] represent index, starting from 0.
|
23 |
+
|
24 |
+

|
25 |
+
|
26 |
+
### Customize
|
27 |
+
You can customize a contract's deployment tx builder by overriding its [buildDeployTransaction](../how-to-write-a-contract/built-ins#builddeploytransaction) method. An example is shown below.
|
28 |
+
|
29 |
+
```ts
|
30 |
+
class DemoContract extends SmartContract {
|
31 |
+
// ...
|
32 |
+
|
33 |
+
// customize the deployment tx by overriding `SmartContract.buildDeployTransaction` method
|
34 |
+
override async buildDeployTransaction(utxos: UTXO[], amount: number,
|
35 |
+
changeAddress?: bsv.Address | string): Promise<bsv.Transaction> {
|
36 |
+
|
37 |
+
const deployTx = new bsv.Transaction()
|
38 |
+
// add p2pkh inputs for paying tx fees
|
39 |
+
.from(utxos)
|
40 |
+
// add contract output
|
41 |
+
.addOutput(new bsv.Transaction.Output({
|
42 |
+
script: this.lockingScript,
|
43 |
+
satoshis: amount,
|
44 |
+
}))
|
45 |
+
// add OP_RETURN output
|
46 |
+
.addData('Hello World')
|
47 |
+
|
48 |
+
if (changeAddress) {
|
49 |
+
deployTx.change(changeAddress);
|
50 |
+
if (this._provider) {
|
51 |
+
deployTx.feePerKb(await this.provider.getFeePerKb())
|
52 |
+
}
|
53 |
+
}
|
54 |
+
|
55 |
+
return deployTx;
|
56 |
+
}
|
57 |
+
}
|
58 |
+
```
|
59 |
+
|
60 |
+
You may visit the [full code](https://github.com/sCrypt-Inc/boilerplate/blob/f63c37038a03bc51267e816d9441969d3e1d2ece/src/contracts/auction.ts#L100-L127) for more details.
|
61 |
+
|
62 |
+
## Call Tx
|
63 |
+
|
64 |
+
### Default
|
65 |
+
For contract calls, the default tx builder creates a transaction with the following structure:
|
66 |
+
|
67 |
+
* Inputs
|
68 |
+
|
69 |
+
* [0]: The input that spends the contract UTXO.
|
70 |
+
* [1β¦]: Zero or more P2PKH inputs for paying transaction fees.
|
71 |
+
|
72 |
+
* Outputs
|
73 |
+
|
74 |
+
* [0β¦N-1]: One or more outputs, each containing a new contract instance (UTXO) if the contract is [stateful](../how-to-write-a-contract/stateful-contract).
|
75 |
+
* [N]: A P2PKH change output if needed.
|
76 |
+
|
77 |
+

|
78 |
+
|
79 |
+
|
80 |
+
### Customize
|
81 |
+
|
82 |
+
You can customize a tx builder for a public `@method` of your contract by calling `bindTxBuilder`. The first parameter is the public method name, and the second parameter is the customized tx builder.
|
83 |
+
|
84 |
+
```ts
|
85 |
+
// bind a customized tx builder for the public method `MyContract.unlock`
|
86 |
+
instance.bindTxBuilder("unlock", (current: T, options: MethodCallOptions<T>, ...args: any) => {
|
87 |
+
// the tx is NOT signed
|
88 |
+
const unsignedTx: bsv.Transaction = new bsv.Transaction()
|
89 |
+
// add contract input
|
90 |
+
.addInput(current.buildContractInput(options.fromUTXO))
|
91 |
+
// add a p2pkh output
|
92 |
+
.addOutput(new bsv.Transaction.Output({
|
93 |
+
script: bsv.Script.fromHex(Utils.buildPublicKeyHashScript(args[0])),
|
94 |
+
satoshis: args[1]
|
95 |
+
}))
|
96 |
+
// add change output
|
97 |
+
.change(options.changeAddress)
|
98 |
+
|
99 |
+
return Promise.resolve({
|
100 |
+
tx: unsignedTx,
|
101 |
+
atInputIndex: 0, // the contract input's index
|
102 |
+
nexts: []
|
103 |
+
})
|
104 |
+
})
|
105 |
+
```
|
106 |
+
|
107 |
+
Note that the parameters of your customized tx builder consist of the following parts:
|
108 |
+
|
109 |
+
- `current` is the actual instance of the smart contract.
|
110 |
+
- `options` is of type [`MethodCallOptions`](../how-to-test-a-contract.md#methodcalloptions).
|
111 |
+
- `...args: any` is an argument list the same as the bound pubic `@method`.
|
112 |
+
|
113 |
+
|
114 |
+
## Notes
|
115 |
+
|
116 |
+
Please be aware that each of these tx builders should only create an **unsigned** transaction. If required, the transaction gets signed automatically in a later step prior to broadcasting.
|
117 |
+
|
118 |
+
Also, your customized tx must satisfy all of the called `@method`'s assertions.
|
how-to-debug-a-contract.md
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 7
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Debug a Contract
|
6 |
+
|
7 |
+
Debugging an sCrypt contract is as easy as debugging TypeScript, since it is just TypeScript.
|
8 |
+
|
9 |
+
|
10 |
+
## Use `console.log()`
|
11 |
+
|
12 |
+
You can use `console.log()` to print to the console.
|
13 |
+
|
14 |
+
|
15 |
+
```ts
|
16 |
+
export class Demo extends SmartContract {
|
17 |
+
|
18 |
+
@prop()
|
19 |
+
readonly x: bigint
|
20 |
+
|
21 |
+
@prop()
|
22 |
+
readonly y: bigint
|
23 |
+
|
24 |
+
constructor(x: bigint, y: bigint) {
|
25 |
+
super(...arguments)
|
26 |
+
this.x = x
|
27 |
+
this.y = y
|
28 |
+
}
|
29 |
+
|
30 |
+
@method()
|
31 |
+
sum(a: bigint, b: bigint): bigint {
|
32 |
+
return a + b
|
33 |
+
}
|
34 |
+
|
35 |
+
@method()
|
36 |
+
public add(z: bigint) {
|
37 |
+
console.log(`z: ${z}`) // print the value of z
|
38 |
+
console.log(`sum: ${this.x + this.y}`) // print the value of this.x + this.y
|
39 |
+
assert(z == this.sum(this.x, this.y), 'incorrect sum')
|
40 |
+
}
|
41 |
+
}
|
42 |
+
```
|
43 |
+
[Try it on Replit](https://replit.com/@msinkec/scryptTS-console-logging)
|
44 |
+
|
45 |
+
After running the code, you should see the following output:
|
46 |
+
|
47 |
+
```
|
48 |
+
z: 3
|
49 |
+
sum: 3
|
50 |
+
```
|
51 |
+
|
52 |
+
|
53 |
+
## Use Visual Studio Code debugger
|
54 |
+
|
55 |
+
You can use VS Code to debug sCrypt contracts, the same way as any other TypeScript programs. If you have created a project with [the sCrypt CLI](installation.md), you should have an auto-generated [launch.json](https://github.com/sCrypt-Inc/boilerplate/blob/master/.vscode/launch.json), containing everything needed for the debugger out of the box. To learn more about the VS Code TypeScript debugger, please refer to the [official documentation](https://code.visualstudio.com/docs/TypeScript/TypeScript-debugging).
|
56 |
+
|
57 |
+

|
58 |
+
You can set some breakpoints and choose `Launch demo` from the `Run and Debug` view (or press **F5**) to start the debugger instantly.
|
59 |
+
|
60 |
+
|
61 |
+

|
62 |
+
|
63 |
+
:::note
|
64 |
+
You need to change the contract file name in [launch.json](https://github.com/sCrypt-Inc/boilerplate/blob/master/.vscode/launch.json#L13) if needed.
|
65 |
+
:::
|
66 |
+
|
67 |
+
### Debug a test
|
68 |
+
If you want to debug a unit test written with the [Mocha](https://mochajs.org) testing framework, choose `Launch demo test` from the `Run and Debug` view.
|
69 |
+
|
70 |
+
|
71 |
+

|
72 |
+
|
73 |
+
:::note
|
74 |
+
You need to change the contract test file name in [launch.json](https://github.com/sCrypt-Inc/boilerplate/blob/master/.vscode/launch.json#L25) if needed.
|
75 |
+
:::
|
how-to-debug-scriptcontext.md
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 4
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Debug ScriptContext Failure
|
6 |
+
|
7 |
+
|
8 |
+
[ScriptContext](../how-to-write-a-contract/scriptcontext.md) enables the logic of the contract to be executed correctly according to the agreement, and the state of the contract to be propagated correctly.
|
9 |
+
|
10 |
+
When it runs incorrectly, you need to master the following methods to locate the error more efficiently.
|
11 |
+
|
12 |
+
|
13 |
+
## hashOutputs assertion failed
|
14 |
+
|
15 |
+
The `hashOutputs` field of `ScriptContext` is the double SHA256 of the serialization of all output amount (8-byte little endian) with scriptPubKey. Through it, we can agree on how the outputs of the transaction calling the contract should be constructed.
|
16 |
+
|
17 |
+
If the output of the transaction is not constructed as required by the contract, then the `hashOutputs` of `ScriptContext` field will not match the the double SHA256 of the `outputs` produced in the code when the contract runs. The following assertion will fail:
|
18 |
+
|
19 |
+
```ts
|
20 |
+
assert(this.ctx.hashOutputs == hash256(outputs), 'hashOutputs mismatch')
|
21 |
+
```
|
22 |
+
|
23 |
+
We all know that if the preimage of the hash is inconsistent, the hash value will not match. When an assertion failure occurs, we can only see two mismatched hash values, and cannot visually see the difference between the preimages of the two hash values (that is, the `outputs` in the contract and the outputs of the transaction).
|
24 |
+
|
25 |
+
|
26 |
+
A function `diffOutputs` in DebugFunctions Interface is provided to directly compare the difference between the outputs argument and all the outputs of the transaction bound by `this.to`, which are serialized and hashed to produce the `hashOutputs` field of `ScriptContext`.
|
27 |
+
|
28 |
+
Just call `this.debug.diffOutputs(outputs)` in the contract:
|
29 |
+
|
30 |
+
```ts
|
31 |
+
this.debug.diffOutputs(outputs) // diff and print the comparison result
|
32 |
+
assert(this.ctx.hashOutputs == hash256(outputs), 'hashOutputs mismatch')
|
33 |
+
```
|
34 |
+
|
35 |
+
and you will see the comparison result:
|
36 |
+
|
37 |
+

|
38 |
+
|
39 |
+
Through the printed comparison results, we can intuitively see that the number of satoshis included in the output calculated in the contract is different from the number of satoshis included in the output actually added when constructing the transaction. Now, we have found the source of the error.
|
how-to-deploy-and-call-a-contract.md
ADDED
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 1
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Deploy & Call a Contract
|
6 |
+
|
7 |
+
|
8 |
+
## Core Concepts
|
9 |
+
After you've finished writing a contract, you can deploy and call it. But first, you should learn how a smart contract interacts with the blockchain. In this section, we will go over some fundamental concepts in detail.
|
10 |
+
|
11 |
+

|
12 |
+
[Credit: moonbeam](https://docs.moonbeam.network/tutorials/eth-api/how-to-build-a-dapp)
|
13 |
+
|
14 |
+
### Compile the Contract
|
15 |
+
|
16 |
+
First, call function `SmartContract.compile()` to compile the contract to Bitcoin script, so it can be included in a transaction's output.
|
17 |
+
|
18 |
+
```ts
|
19 |
+
await MyContract.compile()
|
20 |
+
```
|
21 |
+
|
22 |
+
### Contract Instance
|
23 |
+
As explained in the [Overview section](../overview.md), an `sCrypt` contract is based on the Bitcoin UTXO model. A **constract instance** is an abstraction that represents a specific contract deployed on-chain, so you can use it to interact with the contract like a normal TypeScript object.
|
24 |
+
|
25 |
+
```ts
|
26 |
+
// construct a new instance of `MyContract`
|
27 |
+
let instance = new MyContract(...initArgs);
|
28 |
+
```
|
29 |
+
|
30 |
+
### Provider
|
31 |
+
|
32 |
+
A `Provider` is an abstraction of a standard Bitcoin node that provides connection to the Bitcoin network, for read and write access to the blockchain.
|
33 |
+
|
34 |
+
sCrypt already has a few built-in providers:
|
35 |
+
|
36 |
+
* `DummyProvider`: A mockup provider just for local tests. It does not connect to the Bitcoin blockchain and thus cannot send transactions.
|
37 |
+
|
38 |
+
* `DefaultProvider`: The default provider is the safest, easiest way to begin developing on Bitcoin, and it is also robust enough for use in production. It can be used in testnet as well as mainnet.
|
39 |
+
|
40 |
+
* See full list of providers [here](../reference/classes/Provider.md#hierarchy).
|
41 |
+
|
42 |
+
You can initialize these providers like this:
|
43 |
+
|
44 |
+
```ts
|
45 |
+
let dummyProvider = new DummyProvider();
|
46 |
+
|
47 |
+
// mainnet
|
48 |
+
|
49 |
+
let provider = new DefaultProvider();
|
50 |
+
|
51 |
+
// testnet
|
52 |
+
|
53 |
+
let provider = new DefaultProvider(bsv.Networks.testnet);
|
54 |
+
```
|
55 |
+
|
56 |
+
### Signer
|
57 |
+
|
58 |
+
A `Signer` is an abstraction of private keys, which can be used to sign messages and transactions. A simple signer would be a single private key, while a complex signer is a wallet.
|
59 |
+
|
60 |
+
#### TestWallet
|
61 |
+
|
62 |
+
For testing purposes only, we have a built-in wallet called `TestWallet`. It can be created like this:
|
63 |
+
|
64 |
+
```ts
|
65 |
+
const signer = new TestWallet(privateKey, provider);
|
66 |
+
```
|
67 |
+
|
68 |
+
`privateKey` can be a single private key or an array of private keys that the wallet can use to sign transactions. The ability of the wallet to send transactions is assigned to `provider`. In other words, a `TestWallet` serves as both a signer and a provider.
|
69 |
+
|
70 |
+
|
71 |
+
### Tx Builders
|
72 |
+
|
73 |
+
To deploy or interact with contracts, we must build transactions and broadcast them to Bitcoin.
|
74 |
+
We have some built-in tx builders for the most common way to interact with contracts, so usually you don't have to implement them. If the default tx builder does not meet your specific requirements, such as having extra inputs or outputs in your tx, you can [customize it](./how-to-customize-a-contract-tx.md).
|
75 |
+
|
76 |
+
|
77 |
+
#### Contract Deployment Transaction
|
78 |
+
|
79 |
+
A Bitcoin transaction is required when deploying a contract to the blockchain. The transaction should have an output, whose script is compiled from the contract. This output is known as a contract UTXO and the contract instance comes `from` this UTXO.
|
80 |
+
|
81 |
+
An instance's `from` can be accessed.
|
82 |
+
```ts
|
83 |
+
// the tx that contains the instance
|
84 |
+
instance.from.tx
|
85 |
+
// the index of the tx output that contains the instance
|
86 |
+
instance.from.outputIndex
|
87 |
+
```
|
88 |
+
|
89 |
+
#### Contract Call Transaction
|
90 |
+
|
91 |
+
When you call a public method of a contract instance in a UTXO, a call transaction is needed. The transaction has an input that references to the UTXO and contains the script consisting of the method's arguments. We regard the contract instance goes `to` this transaction input.
|
92 |
+
|
93 |
+
An instance's `to` can be accessed.
|
94 |
+
```ts
|
95 |
+
// the tx that spends the instance
|
96 |
+
instance.to.tx
|
97 |
+
// the index of the tx input that spends the UTXO the instance is in
|
98 |
+
instance.to.inputIndex
|
99 |
+
```
|
100 |
+
|
101 |
+
|
102 |
+
This section could be summarized as the diagram below:
|
103 |
+
|
104 |
+

|
105 |
+
|
106 |
+
|
107 |
+
## Prepare a Signer and Provider
|
108 |
+
|
109 |
+
A signer and a provider must be connected to a contract instance before deployment and call. When we are ready to deploy the contract to the testnet/mainnet, we need a real provider like [DefaultProvider](#provider).
|
110 |
+
|
111 |
+
```ts
|
112 |
+
const network = bsv.Networks.testnet; // or bsv.Networks.mainnet
|
113 |
+
const signer = new TestWallet(privateKey, new DefaultProvider(network));
|
114 |
+
```
|
115 |
+
|
116 |
+
The `privateKey` must have enough coins. Learn how to fund it on a testnet using a [faucet](./faucet).
|
117 |
+
|
118 |
+
Then just connect it to your contract instance like this:
|
119 |
+
|
120 |
+
```ts
|
121 |
+
await instance.connect(signer);
|
122 |
+
```
|
123 |
+
|
124 |
+
|
125 |
+
## Contract Deployment
|
126 |
+
|
127 |
+
To deploy a smart contract, simply call its `deploy` method:
|
128 |
+
|
129 |
+
|
130 |
+
```ts
|
131 |
+
// construct a new instance of `MyContract`
|
132 |
+
let instance = new MyContract(...initArgs);
|
133 |
+
|
134 |
+
// connect the signer to the instance
|
135 |
+
await instance.connect(signer);
|
136 |
+
|
137 |
+
// the contract UTXOβs satoshis
|
138 |
+
const initBalance = 1234;
|
139 |
+
|
140 |
+
// build and send tx for deployment
|
141 |
+
const deployTx = await instance.deploy(initBalance);
|
142 |
+
console.log(`Smart contract successfully deployed with txid ${deployTx.id}`);
|
143 |
+
```
|
144 |
+
|
145 |
+
## Contract Call
|
146 |
+
To facilitate calling a contract's public `@method`, we have injected a runtime object named `methods` in your contract class. For each public `@method` of your contract (e.g., `contract.foo`), a function with the same name and signature (including list of parameters and return type, i.e., void) is added into `methods` (e.g., `contract.methods.foo`). In addition, there is an `options` appended as the last paramter.
|
147 |
+
|
148 |
+
Assume you have a contract like this:
|
149 |
+
|
150 |
+
```ts
|
151 |
+
Class MyContract extends SmartContract {
|
152 |
+
...
|
153 |
+
@method()
|
154 |
+
public foo(arg1, arg2) {...}
|
155 |
+
}
|
156 |
+
```
|
157 |
+
You can check it like this:
|
158 |
+
|
159 |
+
```ts
|
160 |
+
let instance = new MyContract();
|
161 |
+
console.log(typeof instance.methods.foo) // output `function`
|
162 |
+
```
|
163 |
+
|
164 |
+
This function is designed to invoke the corresponding `@method` of the same name on chain, meaning calling it will spend the previous contract UTXO in a new transaction. You can call it like this:
|
165 |
+
|
166 |
+
```ts
|
167 |
+
// Note: `instance.methods.foo` should be passed in all arguments and in the same order that `instance.foo` would take.
|
168 |
+
|
169 |
+
// Additionally, it can accept an optional "options" argument to control the behavior of the function.
|
170 |
+
|
171 |
+
const { tx, atInputIndex } = await instance.methods.foo(arg1, arg2, options);
|
172 |
+
```
|
173 |
+
|
174 |
+
|
175 |
+
What actually happens during the call is the following.
|
176 |
+
|
177 |
+
1. Build an unsigned transaction by calling the tx builder, which can be a default or a customized one introduced in [this section](./how-to-deploy-and-call-a-contract/how-to-customize-a-contract-tx#customizedcalltxbuilder), for a public `@method`.
|
178 |
+
|
179 |
+
2. Use the instance's signer to sign the transaction. Note that `instance.foo` could be invoked during this process in order to get a valid unlocking script for the input.
|
180 |
+
|
181 |
+
3. User the instance's connected provider to send the transaction.
|
182 |
+
|
183 |
+
#### MethodCallOptions
|
184 |
+
|
185 |
+
The `options` argument is of type `MethodCallOptions`:
|
186 |
+
|
187 |
+
```ts
|
188 |
+
/**
|
189 |
+
* A option type to call a contract public `@method` function.
|
190 |
+
* Used to specify the behavior of signers and transaction builders.
|
191 |
+
* For example, specifying a transaction builder to use a specific change address or specifying a signer to use a specific public key to sign.
|
192 |
+
*/
|
193 |
+
export interface MethodCallOptions<T> {
|
194 |
+
/**
|
195 |
+
* The private key(s) associated with these address(es) or public key(s)
|
196 |
+
* must be used to sign the contract input,
|
197 |
+
* and the callback function will receive the results of the signatures as an argument named `sigResponses`
|
198 |
+
* */
|
199 |
+
readonly pubKeyOrAddrToSign?: PublicKeysOrAddressesOption;
|
200 |
+
/** The subsequent contract instance(s) produced in the outputs of the method calling tx in a stateful contract */
|
201 |
+
readonly next?: StatefulNext<T>[] | StatefulNext<T>,
|
202 |
+
/** The `lockTime` of the method calling tx */
|
203 |
+
readonly lockTime?: number;
|
204 |
+
/** The `sequence` of the input spending previous contract UTXO in the method calling tx */
|
205 |
+
readonly sequence?: number;
|
206 |
+
/** The previous contract UTXO to spend in the method calling tx */
|
207 |
+
readonly fromUTXO?: UTXO;
|
208 |
+
/** The P2PKH change output address */
|
209 |
+
readonly changeAddress?: AddressOption;
|
210 |
+
/** verify the input script before send transaction */
|
211 |
+
readonly verify?: boolean;
|
212 |
+
/** Whether to call multiple contracts at the same time in one transaction */
|
213 |
+
readonly multiContractCall?: true;
|
214 |
+
/** Pass the `ContractTransaction` of the previous call as an argument to the next call, only used if `multiContractCall = true`. */
|
215 |
+
readonly partialContractTx?: ContractTransaction;
|
216 |
+
}
|
217 |
+
```
|
218 |
+
|
219 |
+
The major differences between here and [local tests](../how-to-test-a-contract.md#test-a-contract-locally) are:
|
220 |
+
1. the contract needs to be deployed first;
|
221 |
+
2. the contract instance is connected to a real provider, which broadcasts transactions to the blockchain.
|
222 |
+
|
223 |
+
### Create a smart contract instance from a transaction
|
224 |
+
To interact with a deployed smart contract (i.e., calling its public methods), we need its contract instance corresponding to its latest state on chain, stateful or not. When testing on testnet, we usually put a contract's deployment and its calling (note there could be multiple calls if the contract is stateful) in the same process for convenience, so that we don't need to manage the internal state of the instance manually, because it's always consistent with the transactions on chain.
|
225 |
+
|
226 |
+
In reality, a contract's deployment and its call, and its different calls in the case of a stateful contract, may well be in separate processes. For example, the deployment party is different from the calling party, or multiple parties call it. If so, we need to create a contract instance from an on-chain transaction that represents its latest state, before we can call its method.
|
227 |
+
|
228 |
+
Typically, we only know the [TXID](https://wiki.bitcoinsv.io/index.php/TXID) of the transaction containing the instance. We can create an instance in two steps:
|
229 |
+
1. Using TXID, we retrieve the full transaction by calling [getTransaction](../reference/classes/Provider.md#gettransaction) of the [connected provider](../reference/classes/Signer.md#connectedprovider) of the signer.
|
230 |
+
2. We can create an contract instance from a transaction's by calling [fromTx()](../how-to-write-a-contract/built-ins.md#fromtx).
|
231 |
+
```ts
|
232 |
+
// 1) fetch a transaction from txid
|
233 |
+
const tx = await signer.connectedProvider.getTransaction(txId)
|
234 |
+
// 2) create instance from transaction
|
235 |
+
const instance = Counter.fromTx(tx, atOutputIndex)
|
236 |
+
|
237 |
+
// from now on, `instance` is in sync with the on-chain transaction
|
238 |
+
// and we can use it to interact with the contract
|
239 |
+
```
|
240 |
+
|
241 |
+
A complete example can be found [here](./call-deployed).
|
242 |
+
|
243 |
+
### Method with Signatures
|
244 |
+
|
245 |
+
A contract public `@method` often needs a signature argument for authentication. Take this [Pay To Pubkey Hash (P2PKH)](https://learnmeabitcoin.com/technical/p2pkh) contract for example:
|
246 |
+
|
247 |
+
```ts
|
248 |
+
export class P2PKH extends SmartContract {
|
249 |
+
@prop()
|
250 |
+
readonly pubKeyHash: PubKeyHash;
|
251 |
+
|
252 |
+
constructor(pubKeyHash: PubKeyHash) {
|
253 |
+
super(..arguments);
|
254 |
+
this.pubKeyHash = pubKeyHash;
|
255 |
+
}
|
256 |
+
|
257 |
+
@method()
|
258 |
+
public unlock(sig: Sig, pubkey: PubKey) {
|
259 |
+
// make sure the `pubkey` is the one locked with its hash value in the constructor
|
260 |
+
assert(hash160(pubkey) == this.pubKeyHash, 'pubKeyHash check failed');
|
261 |
+
|
262 |
+
// make sure the `sig` is signed by the private key corresponding to the `pubkey`
|
263 |
+
assert(this.checkSig(sig, pubkey), 'signature check failed');
|
264 |
+
}
|
265 |
+
}
|
266 |
+
```
|
267 |
+
|
268 |
+
We can call the `unlock` method like this:
|
269 |
+
|
270 |
+
```ts
|
271 |
+
// call
|
272 |
+
const { tx: callTx } = await p2pkh.methods.unlock(
|
273 |
+
// the first argument `sig` is replaced by a callback function which will return the needed signature
|
274 |
+
(sigResps) => findSig(sigResps, publicKey),
|
275 |
+
|
276 |
+
// the second argument is still the value of `pubkey`
|
277 |
+
PubKey(toHex(publicKey)),
|
278 |
+
|
279 |
+
// method call options
|
280 |
+
{
|
281 |
+
// A request for signer to sign with the private key corresponding to a public key
|
282 |
+
pubKeyOrAddrToSign: publicKey
|
283 |
+
} as MethodCallOptions<P2PKH>
|
284 |
+
);
|
285 |
+
|
286 |
+
console.log('contract called: ', callTx.id);
|
287 |
+
|
288 |
+
```
|
289 |
+
|
290 |
+
When `p2phk.method.unlock` is called, the option contains `pubKeyOrAddrToSign`, requesting a signature against `publicKey`.
|
291 |
+
|
292 |
+
The first argument is a signature, which can be obtained in a callback function. The function takes a list of signatures requested in `pubKeyOrAddrToSign` and find the one signature to the right public key/address.
|
293 |
+
|
294 |
+
In general, if your `@method` needs `Sig`-typed arguments, you could obtain them as follows:
|
295 |
+
|
296 |
+
1. Ensure that the `pubKeyOrAddrToSign` contains all public keys/addresses corresponding to these `Sig`s;
|
297 |
+
|
298 |
+
2. Replace each `Sig` argument with a callback function that filters to the right `Sig` from the full list of signature in `sigResps`.
|
299 |
+
|
300 |
+
|
301 |
+
## Example
|
302 |
+
|
303 |
+
Here is the complete sample code for the deployment and call of a P2PKH contract.
|
304 |
+
|
305 |
+
```ts
|
306 |
+
import { privateKey } from '../../utils/privateKey';
|
307 |
+
|
308 |
+
// compile contract
|
309 |
+
await P2PKH.compile()
|
310 |
+
|
311 |
+
// public key of the `privateKey`
|
312 |
+
const publicKey = privateKey.publicKey
|
313 |
+
// hash of the `publicKey`
|
314 |
+
const pkh = bsv.crypto.Hash.sha256ripemd160(publicKey.toBuffer())
|
315 |
+
|
316 |
+
// setup signer
|
317 |
+
const signer = new TestWallet(privateKey, new DefaultProvider());
|
318 |
+
|
319 |
+
// initialize an instance with `pkh`
|
320 |
+
let p2pkh = new P2PKH(PubKeyHash(toHex(pkh)))
|
321 |
+
|
322 |
+
// connect the signer
|
323 |
+
await p2pkh.connect(signer);
|
324 |
+
|
325 |
+
// deploy the contract, with 1 satoshi locked in
|
326 |
+
const deployTx = await p2pkh.deploy(1);
|
327 |
+
console.log('contract deployed: ', deployTx.id);
|
328 |
+
|
329 |
+
// call
|
330 |
+
const { tx: callTx } = await p2pkh.methods.unlock(
|
331 |
+
(sigResps) => findSig(sigResps, publicKey),
|
332 |
+
PubKey(toHex(publicKey)),
|
333 |
+
{
|
334 |
+
pubKeyOrAddrToSign: publicKey
|
335 |
+
} as MethodCallOptions<P2PKH>
|
336 |
+
);
|
337 |
+
|
338 |
+
console.log('contract called: ', callTx.id);
|
339 |
+
|
340 |
+
```
|
341 |
+
|
342 |
+
More examples can be found [here](https://github.com/sCrypt-Inc/boilerplate/tree/master/tests/testnet).
|
343 |
+
|
344 |
+
|
345 |
+
### Running the code
|
346 |
+
|
347 |
+
The deployment and call code is wrapped into a simple NPM command:
|
348 |
+
|
349 |
+
```sh
|
350 |
+
npm run testnet
|
351 |
+
```
|
352 |
+
|
353 |
+
Make sure you fund your address before running this command.
|
354 |
+
After a successful run you should see something like the following:
|
355 |
+
|
356 |
+
```
|
357 |
+
P2PKH contract deployed: f3f372aa25f159efa93db8c51a4eabbb15935358417ffbe91bfb78f4f0b1d2a3
|
358 |
+
P2PKH contract called: dc53da3e80aadcdefdedbeb6367bb8552e381e92b226ab1dc3dc9b3325d8a8ee
|
359 |
+
```
|
360 |
+
|
361 |
+
These are the TXIDs of the transaction which deployed the smart contract and then the one which called its method. You can see the transactions using a [block explorer](https://test.whatsonchain.com/tx/f3f372aa25f159efa93db8c51a4eabbb15935358417ffbe91bfb78f4f0b1d2a3).
|
362 |
+
|
363 |
+
|
364 |
+
### Customize Transactions
|
365 |
+
Deploying and calling a contract builds transactions with a certain format, which suffices for many cases. In cases where the tx format does not work for you and you need to customize it, please refer to [this section](./how-to-customize-a-contract-tx.md).
|
how-to-integrate-a-frontend.md
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 8
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Integrate With a Frontend
|
6 |
+
|
7 |
+
This section will show how to integrate your smart contract to a frontend, so users can interact with it.
|
8 |
+
|
9 |
+
We use [React](https://reactjs.org/) as our frontend framework as an example. We assume that you already have the basic knowledge of frontend development, so we will not spend much time introducing this part of the code, but mostly be focusing on how to interact with the smart contract in the front end project.
|
10 |
+
|
11 |
+
:::note
|
12 |
+
Currently, the only supported frontend frameworks is React. We anticipate to add supports for other frameworks over time.
|
13 |
+
:::
|
14 |
+
|
15 |
+
## Setup
|
16 |
+
|
17 |
+
### React
|
18 |
+
|
19 |
+
Run the following command to create a React project, named `helloworld`.
|
20 |
+
|
21 |
+
```bash
|
22 |
+
npx create-react-app helloworld --template typescript
|
23 |
+
```
|
24 |
+
|
25 |
+

|
26 |
+
|
27 |
+
We will do most work under the `src` directory.
|
28 |
+
|
29 |
+
### `sCrypt`
|
30 |
+
|
31 |
+
Run the `init` command of the [CLI](../installation.md#the-scrypt-cli-tool) to turn it into an `sCrypt` project.
|
32 |
+
|
33 |
+
```bash
|
34 |
+
cd helloworld
|
35 |
+
npx scrypt-cli init
|
36 |
+
```
|
37 |
+
|
38 |
+
This installs all the dependencies and configs the contract development environment.
|
39 |
+
After this, we are ready to go!
|
40 |
+
|
41 |
+
## Load Contract
|
42 |
+
|
43 |
+
Before interacting with a smart contract at the front end, we need to load the contract class in two steps.
|
44 |
+
|
45 |
+
|
46 |
+
We'll take a look at how to generate the artifact by ourselves first.
|
47 |
+
|
48 |
+
### 1. Compile Contract
|
49 |
+
|
50 |
+
Before you start, you need to get the contract source files, as a frontend developer.
|
51 |
+
|
52 |
+
Let's use the [Helloworld contract](../tutorials/hello-world.md) as an example. Copy and paste `helloworld.ts` into the `src/contracts` directory.
|
53 |
+
|
54 |
+

|
55 |
+
|
56 |
+
Run the following command to compile the contract.
|
57 |
+
|
58 |
+
```bash
|
59 |
+
npx scrypt-cli compile
|
60 |
+
```
|
61 |
+
|
62 |
+

|
63 |
+
|
64 |
+
After the compilation, you will get an JSON artifact file at `artifacts/src/contracts/helloworld.json`.
|
65 |
+
|
66 |
+

|
67 |
+
|
68 |
+
### 2. Load Artifact
|
69 |
+
|
70 |
+
Now with the contract artifact file, you directly load it in the `index.tsx` file.
|
71 |
+
|
72 |
+
```ts
|
73 |
+
import { Helloworld } from './contracts/helloworld';
|
74 |
+
import artifact from '../artifacts/src/contracts/helloworld.json';
|
75 |
+
Helloworld.loadArtifact(artifact);
|
76 |
+
```
|
77 |
+
|
78 |
+
Now you can create an instance from the contract class as before.
|
79 |
+
```ts
|
80 |
+
const message = toByteString('hello world', true)
|
81 |
+
const instance = new Helloworld(sha256(message))
|
82 |
+
```
|
83 |
+
|
84 |
+
:::info
|
85 |
+
You cannot simply call `Helloworld.compile()` at the front end, since it only works in NodeJS, not in browser.
|
86 |
+
:::
|
87 |
+
|
88 |
+
## Integrate Wallet
|
89 |
+
|
90 |
+
You will integrate [Sensilet](https://sensilet.com/), a browser extension wallet similar to [MetaMask](https://metamask.io/), into the project.
|
91 |
+
|
92 |
+
:::info
|
93 |
+
You can refer to this [guide](../advanced/how-to-add-a-signer.md) to add support for other wallets.
|
94 |
+
:::
|
95 |
+
|
96 |
+
To request access to the wallet, you can use its `requestAuth` method.
|
97 |
+
|
98 |
+
```ts
|
99 |
+
const provider = new DefaultProvider({
|
100 |
+
network: bsv.Networks.testnet
|
101 |
+
});
|
102 |
+
|
103 |
+
const signer = new SensiletSigner(provider);
|
104 |
+
|
105 |
+
// request authentication
|
106 |
+
const { isAuthenticated, error } = await signer.requestAuth();
|
107 |
+
if (!isAuthenticated) {
|
108 |
+
// something went wrong, throw an Error with `error` message
|
109 |
+
throw new Error(error);
|
110 |
+
}
|
111 |
+
|
112 |
+
// authenticated
|
113 |
+
// you can show user's default address
|
114 |
+
const userAddress = await signer.getDefaultAddress();
|
115 |
+
// ...
|
116 |
+
```
|
117 |
+
|
118 |
+
Now you can connect the wallet to the contract instance as before.
|
119 |
+
```ts
|
120 |
+
await instance.connect(signer);
|
121 |
+
```
|
122 |
+
|
123 |
+
Afterwards, you can interact with the contract from the front end by [calling its method](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#contract-call) as usual.
|
124 |
+
|
125 |
+
Go [here](https://academy.scrypt.io) to see a full example on how to build a Tic-Tac-Toe game on chain.
|
how-to-integrate-dotwallet.md
ADDED
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# How to integrate DotWallet
|
2 |
+
|
3 |
+
|
4 |
+
[DotWallet](https://www.dotWallet.com/en) is a lightweight wallet designed to help users easily and securely manage their digital assets. We will show how to integrate it with sCrypt-powered apps.
|
5 |
+
|
6 |
+
|
7 |
+
## OAuth 2.0
|
8 |
+
|
9 |
+
OAuth 2.0 is an industry-standard authorization framework that enables third-party applications to access the resources of a user on a web service β- such as Facebook, Google, and Twitter -- without requiring the user to share their credentials directly with the application. It provides a secure and standardized way for users to grant limited access to their protected resources, such as their profile information or photos, to other applications.
|
10 |
+
It works by introducing an authorization layer between the user, the application, and the web service hosting the user's data. Instead of sharing their username and password with the application, the user is redirected to the web service's authentication server. The user then authenticates themselves on the service, and upon successful authentication, the service issues an access token to the application. This access token represents the user's authorization to access specific resources.
|
11 |
+
|
12 |
+
If you are new to OAuth 2.0, check out thse helpful tutorials:
|
13 |
+
- [An Illustrated Guide to OAuth and OpenID Connect](https://developer.okta.com/blog/2019/10/21/illustrated-guide-to-oauth-and-oidc)
|
14 |
+
- [The Simplest Guide To OAuth 2.0](https://darutk.medium.com/the-simplest-guide-to-oauth-2-0-8c71bd9a15bb)
|
15 |
+
- [An Introduction to OAuth 2](https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2)
|
16 |
+
|
17 |
+
## DotWallet's user authorization
|
18 |
+
|
19 |
+
DotWallet uses OAuth 2.0 to allow third-party applications to safely access certain capabilities authorized by DotWallet users. More specifically, it uses Oauth2's authorization code grant type as the diagram shows. See [RFC6749](https://tools.ietf.org/html/rfc6749#section-4.1) for details.
|
20 |
+
|
21 |
+

|
22 |
+
|
23 |
+
[Credit: Vihanga Liyanage](https://medium.com/@vihanga_liyanage/iam-for-dummies-oauth-2-grant-types-397197a26024)
|
24 |
+
|
25 |
+
Follow [these steps](https://developers.dotwallet.com/documents/en/#user-authorization) for a user authorization.
|
26 |
+
|
27 |
+
1. Construct URI.
|
28 |
+
|
29 |
+
Example URI: `https://api.ddpurse.com/v1/oauth2/authorize?client_id=YOUR-CLIENT-ID&redirect_uri=http%3A%2F%2FYOUR-REDIRECT-URL&response_type=code&state=YOUR-STATE&scope=user.info`
|
30 |
+
|
31 |
+
URL Parameters:
|
32 |
+
|
33 |
+
| Parameter | Description |
|
34 |
+
| -------- | ------- |
|
35 |
+
| client_id | Developerβs dapp client_id |
|
36 |
+
| redirect_uri | The redirect URL after authorization. Needs to be url_encoded |
|
37 |
+
| state | It is recommended to use a random string of more than 32 bits (such as UUID). The state is used to verify the consistency of the request and callback. This can prevent csrf attacks. |
|
38 |
+
|response_type | Fill in the fixed value : `code` |
|
39 |
+
|scope | Authorization scope. The list of permissions that the user agrees to authorize. These permissions are required for certain API endpoints. Needs to be url_encoded. Use spaces to separate multiple permissions. For a list of currently supported scope permissions, please check the scope list [here](https://developers.dotwallet.com/documents/en/#user-authorization)|
|
40 |
+
|
41 |
+
2. Redirect the user to the URI constructed in step 1
|
42 |
+
|
43 |
+
After clicking the link, the user will be directed to the DotWallet authorization page. DotWallet will ask the user to log in, and then ask whether they agree to authorize the application for the listed permission scopes.
|
44 |
+
|
45 |
+
3. Receive the `code` through the callback uri.
|
46 |
+
|
47 |
+
After the user agrees to authorization in step 2, DotWallet will redirect the client to the `redirect_uri` specified by the application. The authorization code `code` and the provided `state` will be included in the query parameters.
|
48 |
+
|
49 |
+
4. Exchange `code` for access_token. The access tokens are credentials used to access protected resources, which are issued by the authorization server.
|
50 |
+
|
51 |
+
:::warning
|
52 |
+
To avoid security issues, any request for using or obtaining `access_token` must be made from the backend server. Do not disclose your `access_token` and `client_secret`<sup>1</sup> on the client side.
|
53 |
+
:::
|
54 |
+
|
55 |
+
|
56 |
+
### DotWallet Developer Platform
|
57 |
+
|
58 |
+
1. Before using DotWallet, you need to register and create an app on [DotWallet Developer Platform](https://developers.dotwallet.com/en).
|
59 |
+
|
60 |
+

|
61 |
+
|
62 |
+
2. After creating the app, you will receive an email containing `app_id` and `secret`.
|
63 |
+
|
64 |
+
|
65 |
+

|
66 |
+
|
67 |
+
1. Next, you need to set [redirection URI](https://www.oauth.com/oauth2-servers/redirect-uris). Redirect URLs are a critical part of the OAuth flow. After a user successfully authorizes an application, the authorization server will redirect the user back to the application. For example, in the figure below `http://localhost:3000/callback/` is the redirection.
|
68 |
+
|
69 |
+

|
70 |
+
|
71 |
+
|
72 |
+
:::note
|
73 |
+
*Callback domain* in the form is the redirection URIs in OAuth.
|
74 |
+
:::
|
75 |
+
|
76 |
+
## Example Implementation
|
77 |
+
|
78 |
+
|
79 |
+
Here is an example to integration DotWallet in [Nextjs](https://nextjs.org/), a popular React development framework.
|
80 |
+
|
81 |
+
1. Construct URI.
|
82 |
+
|
83 |
+
```ts
|
84 |
+
export default async function Home() {
|
85 |
+
const client_id = process.env.CLIENT_ID;
|
86 |
+
const redirect_uri = encodeURIComponent(process.env.REDIRECT_URI || '');
|
87 |
+
const scope = encodeURIComponent("user.info autopay.bsv");
|
88 |
+
const state = crypto.randomUUID();
|
89 |
+
const loginUrl = `https://api.ddpurse.com/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=code&scope=${scope}&state=${state}`;
|
90 |
+
|
91 |
+
return (
|
92 |
+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
93 |
+
<div className="m-4 p-4 bg-blue-200 font-bold rounded-lg">
|
94 |
+
<a href={loginUrl}>DotWallet Login</a>
|
95 |
+
</div>
|
96 |
+
</main>
|
97 |
+
);
|
98 |
+
}
|
99 |
+
```
|
100 |
+
|
101 |
+
<center>src/app/page.tsx</center>
|
102 |
+
|
103 |
+
|
104 |
+
If the user clicks the **DotWallet Login** link, the page will be redirected to the wallet authorization page.
|
105 |
+
|
106 |
+

|
107 |
+
|
108 |
+
|
109 |
+
2. After the user clicks **Agree to authorize** to log in, the authorization server redirects the user to the redirection URI. The following code receives the `code` through the callback uri, exchanges the `code` for `access_token` and save it.
|
110 |
+
|
111 |
+
Inside the `app` directory, folders are used to [define routes](https://nextjs.org/docs/app/building-your-application/routing/defining-routes#creating-routes) in nextjs. we create `src/app/callback/route.ts` to handle the redirection request.
|
112 |
+
|
113 |
+
```ts
|
114 |
+
import { redirect, notFound } from 'next/navigation';
|
115 |
+
|
116 |
+
import token from "../token"
|
117 |
+
|
118 |
+
export async function GET(request: Request) {
|
119 |
+
const { searchParams } = new URL(request.url);
|
120 |
+
const code = searchParams.get('code');
|
121 |
+
|
122 |
+
if (code) {
|
123 |
+
// exchange the code for access_token
|
124 |
+
const res = await fetch(`https://api.ddpurse.com/v1/oauth2/get_access_token`, {
|
125 |
+
body: JSON.stringify({
|
126 |
+
code,
|
127 |
+
redirect_uri: process.env.REDIRECT_URI,
|
128 |
+
grant_type: "authorization_code",
|
129 |
+
client_secret: process.env.CLIENT_SECRET,
|
130 |
+
client_id: process.env.CLIENT_ID,
|
131 |
+
}),
|
132 |
+
headers: {
|
133 |
+
'Content-Type': 'application/json',
|
134 |
+
},
|
135 |
+
method: 'POST'
|
136 |
+
});
|
137 |
+
const { code: apiCode, data, msg } = await res.json();
|
138 |
+
|
139 |
+
if (apiCode === 0) {
|
140 |
+
const { access_token } = data;
|
141 |
+
// save access_token
|
142 |
+
token.access_token = access_token;
|
143 |
+
// redirect to balance page.
|
144 |
+
redirect('/balance');
|
145 |
+
}
|
146 |
+
|
147 |
+
}
|
148 |
+
|
149 |
+
notFound();
|
150 |
+
}
|
151 |
+
```
|
152 |
+
|
153 |
+
<center>src/app/callback/route.ts</center>
|
154 |
+
|
155 |
+
|
156 |
+
### `DotWalletSigner`
|
157 |
+
|
158 |
+
sCrypt SDK provides `DotWalletSigner` for quick integration with DotWallet.
|
159 |
+
|
160 |
+
After redirect to the `/balance` page, we can create a `DotWalletSigner` with the OAuth access token, which is passed as the first argument.
|
161 |
+
|
162 |
+
|
163 |
+
```ts
|
164 |
+
import { DotwalletSigner, DefaultProvider } from "scrypt-ts";
|
165 |
+
import token from "../token";
|
166 |
+
|
167 |
+
async function getData() {
|
168 |
+
const provider = new DefaultProvider();
|
169 |
+
const signer = new DotwalletSigner(token.access_token, provider);
|
170 |
+
|
171 |
+
const balance = await signer.getBalance();
|
172 |
+
|
173 |
+
return { balance: balance.confirmed + balance.unconfirmed };
|
174 |
+
}
|
175 |
+
|
176 |
+
export default async function Balance() {
|
177 |
+
const data = await getData();
|
178 |
+
|
179 |
+
return (
|
180 |
+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
181 |
+
<div className="m-4 p-4 bg-blue-200 font-bold rounded-lg">
|
182 |
+
<label>balance</label> {data.balance}
|
183 |
+
</div>
|
184 |
+
</main>
|
185 |
+
);
|
186 |
+
}
|
187 |
+
|
188 |
+
```
|
189 |
+
|
190 |
+
<center>src/app/balance/page.tsx</center>
|
191 |
+
|
192 |
+
After creating `DotWalletSigner` with access token, you can call all interfaces of `DotWalletSigner` as in other [signers](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#signer).
|
193 |
+
For example, the example uses the signer to check user's balance.
|
194 |
+
|
195 |
+
Congrats! You have completed the integration of DotWallet. Full code is [here](https://github.com/zhfnjust/dotwallet-example).
|
196 |
+
|
197 |
+
------------------------
|
198 |
+
|
199 |
+
[1] `client_secret` is stored in the backend. It's used to exchange authorization code for access token.
|
how-to-integrate-scrypt-service.md
ADDED
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 5
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Integrate sCrypt Service
|
6 |
+
|
7 |
+
Before interacting with a `sCrypt` contract, we must create a contract instance representing the latest state of the contract on chain. Such an instance can be created by calling the [`fromTx`](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#create-a-smart-contract-instance-from-a-transaction) method. However, this means your application needs to track and record all contract-related transactions, especially for a stateful contract.
|
8 |
+
|
9 |
+
An easier alternative is to leverage `sCrypt` infrastructure service, which tracks such transactions, so you can focus on your application logic.
|
10 |
+
|
11 |
+
## Get Your API Key
|
12 |
+
|
13 |
+
### Step 1: Create Your Free Account
|
14 |
+
|
15 |
+
Go to the [sCrypt homepage](https://scrypt.io) to create your free account.
|
16 |
+
|
17 |
+

|
18 |
+
|
19 |
+
### Step 2: Get API Key
|
20 |
+
|
21 |
+
Sign in and click on the copy icon to copy your API Key.
|
22 |
+
|
23 |
+

|
24 |
+
|
25 |
+
## Integration
|
26 |
+
|
27 |
+
Once you have an API key, you can easily integrate sCrypt service into your app by following these simple steps.
|
28 |
+
|
29 |
+
### Step 1: Initialize Client
|
30 |
+
|
31 |
+
You can pass the API key, along with `network`, to the `Scrypt.init` function to initialize an sCrypt client in your app.
|
32 |
+
|
33 |
+
```ts
|
34 |
+
import { Scrypt, bsv } from 'scrypt-ts'
|
35 |
+
|
36 |
+
Scrypt.init({
|
37 |
+
apiKey: 'YOUR_API_KEY',
|
38 |
+
network: bsv.Networks.testnet,
|
39 |
+
})
|
40 |
+
```
|
41 |
+
|
42 |
+
### Step 2: Connect `ScryptProvider` with your signer
|
43 |
+
|
44 |
+
Connect signer to `ScryptProvider`, the required [provider](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#provider) to use sCrypt service.
|
45 |
+
|
46 |
+
```ts
|
47 |
+
const signer = new TestWallet(myPrivateKey)
|
48 |
+
await signer.connect(new ScryptProvider())
|
49 |
+
```
|
50 |
+
|
51 |
+
### Step 3: Get Contract ID
|
52 |
+
|
53 |
+
Each contract is uniquely identified by the transaction that [deploy](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#contract-deployment) it and the output it is in, which we regard as its ID.
|
54 |
+
|
55 |
+
```ts
|
56 |
+
const counter = new Counter(0n)
|
57 |
+
// connect signer
|
58 |
+
await counter.connect(signer)
|
59 |
+
|
60 |
+
const balance = 1
|
61 |
+
const deployTx = await counter.deploy(balance)
|
62 |
+
console.log('contract Counter deployed: ', deployTx.id)
|
63 |
+
|
64 |
+
const contractId = {
|
65 |
+
/** The deployment transaction id */
|
66 |
+
txId: deployTx.id,
|
67 |
+
/** The output index */
|
68 |
+
outputIndex: 0,
|
69 |
+
}
|
70 |
+
```
|
71 |
+
|
72 |
+
You can usually get the ID of a contract from its creator, who publicizes it so others can interact with it.
|
73 |
+
|
74 |
+
### Step 4: Get Contract Instance
|
75 |
+
|
76 |
+
Once you have the contract ID, you can easily create a contract instance as follows.
|
77 |
+
|
78 |
+
```ts
|
79 |
+
const currentInstance = await Scrypt.contractApi.getLatestInstance(
|
80 |
+
Counter,
|
81 |
+
contractId
|
82 |
+
)
|
83 |
+
|
84 |
+
// connect signer
|
85 |
+
await currentInstance.connect(signer)
|
86 |
+
```
|
87 |
+
For a stateless contract, the instance points to the deployment tx; for a stateful one, it points to the latest tip in a chain of txs, which sCrypt service tracks automatically.
|
88 |
+
|
89 |
+
## Interact with the Contract
|
90 |
+
Once you have the instance after following the steps above, you can easily read from the contract, write to it, and listen to it.
|
91 |
+
|
92 |
+
### Read
|
93 |
+
|
94 |
+
You read an instance's properties using the dot operator, like any other object.
|
95 |
+
|
96 |
+
```ts
|
97 |
+
// read @prop count
|
98 |
+
console.log(counter.count)
|
99 |
+
```
|
100 |
+
|
101 |
+
:::note
|
102 |
+
Reading does NOT broadcast a transaction to the blockchain.
|
103 |
+
:::
|
104 |
+
|
105 |
+
### Write
|
106 |
+
|
107 |
+
To update a contract instance, you call its public method as [before](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#contract-call), which writes to the blockchain by broadcasting a transaction.
|
108 |
+
|
109 |
+
```ts
|
110 |
+
// call the method of current instance to apply the updates on chain
|
111 |
+
const { tx } = await currentInstance.methods.incrementOnChain()
|
112 |
+
|
113 |
+
console.log(`Counter contract called, tx: ${tx.id}`)
|
114 |
+
```
|
115 |
+
|
116 |
+
### Listen to Events
|
117 |
+
|
118 |
+
Often, your app needs to be notified when a contract gets called and updated. It is essential to be able to listen to such events in real time that can alert your app whenever something relevant occurs on chain. For example, in your front-end, you can refresh the web page to show the user the latest state of a contract, upon event notifications.
|
119 |
+
|
120 |
+
With the `sCrypt` service, you can easily subscribe to a contract's events by its contract ID, using the `Scrypt.contractApi.subscribe` method. It takes two parameters:
|
121 |
+
|
122 |
+
1. `options: SubscribeOptions<T>`: it includes a contract class, a contract ID, and a optional list of method names monitored.
|
123 |
+
|
124 |
+
```ts
|
125 |
+
interface SubscribeOptions<T> {
|
126 |
+
clazz: new (...args: any) => T;
|
127 |
+
id: ContractId;
|
128 |
+
methodNames?: Array<string>;
|
129 |
+
}
|
130 |
+
```
|
131 |
+
|
132 |
+
If `methodNames` is set, you will be notified only when public functions in the list are called. Otherwise, you will be notified when ANY public function is called.
|
133 |
+
|
134 |
+
2. `callback: (event: ContractCalledEvent<T>) => void`: a callback funciton upon receiving notifications.
|
135 |
+
|
136 |
+
`ContractCalledEvent<T>` contains relevant information on how the contract is called:
|
137 |
+
|
138 |
+
- `methodName: string`, which public method is called
|
139 |
+
|
140 |
+
- `args: SupportedParamType[]`, arguments the public method is called with
|
141 |
+
|
142 |
+
- `tx: bsv.Transaction`, transaction where contract is called from
|
143 |
+
|
144 |
+
- `nexts: Array[T]`, includes the new contract instances created by this call. If a stateful contract is called, `nexts` contains the contract instances containing the new state generated by this call. You can read the latest state from the new contract instance to, e.g., display the new state to users. If a stateless contract is called, `nexts` is empty.
|
145 |
+
|
146 |
+
Below is an example of listening to events when `incrementOnChain` method is called.
|
147 |
+
|
148 |
+
```ts
|
149 |
+
const subscription = Scrypt.contractApi.subscribe({
|
150 |
+
clazz: Counter, // contract class
|
151 |
+
id: contractId, // contract id
|
152 |
+
methodNames: ['incrementOnChain']
|
153 |
+
}, (event: ContractCalledEvent<Counter>) => {
|
154 |
+
// callback when receiving a notification
|
155 |
+
console.log(`${event.methodName} is called with args: ${event.args}`)
|
156 |
+
});
|
157 |
+
```
|
how-to-publish-a-contract.md
ADDED
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 9
|
3 |
+
---
|
4 |
+
|
5 |
+
|
6 |
+
# How to Publish a Contract to NPM
|
7 |
+
|
8 |
+
## What is a Smart Contract Library?
|
9 |
+
|
10 |
+
A smart contract library can provide methods which can be reused in many contracts. Developers can use existing libraries to reduce the cost of developing their own contracts.
|
11 |
+
|
12 |
+
A smart contract library is different from a smart contract in these ways:
|
13 |
+
|
14 |
+
* A smart contract library can not have any public/entry `@method`s, which means a library can not be deployed or called directly through a tx. They can only be called within a smart contract or another library.
|
15 |
+
|
16 |
+
* A smart contract library can not have any stateful properties, i.e. `@prop(true)` properties. But a property declared as `@prop()` is fine.
|
17 |
+
|
18 |
+
## Write a Smart Contract Library
|
19 |
+
|
20 |
+
Using `sCrypt` we can create a smart contract library class like this:
|
21 |
+
|
22 |
+
```ts
|
23 |
+
class MyLib extends SmartContractLib {
|
24 |
+
|
25 |
+
@prop()
|
26 |
+
readonly buf: ByteString;
|
27 |
+
|
28 |
+
constructor(buf: ByteString) {
|
29 |
+
super(...arguments);
|
30 |
+
this.buf = buf;
|
31 |
+
}
|
32 |
+
|
33 |
+
@method()
|
34 |
+
append(content: ByteString) {
|
35 |
+
this.buf += content;
|
36 |
+
}
|
37 |
+
|
38 |
+
@method()
|
39 |
+
static add(x: bigint, y: bigint): bigint {
|
40 |
+
return x + y;
|
41 |
+
}
|
42 |
+
|
43 |
+
}
|
44 |
+
```
|
45 |
+
|
46 |
+
A smart contract library can be declared as a class that extends `SmartContractLib`. It may also have `@prop`s and `@method`s like smart contracts which have the same rules [introduced before](./how-to-write-a-contract). A smart contract library can be used within `@method`s like this:
|
47 |
+
|
48 |
+
```ts
|
49 |
+
class MyContract extends SmartContract {
|
50 |
+
@method()
|
51 |
+
public unlock(x: ByteString) {
|
52 |
+
let myLib = new MyLib(hexToByteString('0123'));
|
53 |
+
myLib.append(x);
|
54 |
+
assert(MyLib.add(1n, 2n) === 3n, 'incorrect sum');
|
55 |
+
}
|
56 |
+
}
|
57 |
+
```
|
58 |
+
|
59 |
+
## Test a Smart Contract Library
|
60 |
+
|
61 |
+
You can test your smart contract library as a normal class, for example, writing some unit tests:
|
62 |
+
|
63 |
+
```ts
|
64 |
+
describe('Test SmartContractLib `MyLib`', () => {
|
65 |
+
it('should pass unit test successfully.', () => {
|
66 |
+
expect(MyLib.add(1n, 2n)).to.eq(3n)
|
67 |
+
})
|
68 |
+
})
|
69 |
+
```
|
70 |
+
|
71 |
+
Also you can write a smart contract using the library, then have some tests for the contract, like:
|
72 |
+
|
73 |
+
```ts
|
74 |
+
class TestLib extends SmartContract {
|
75 |
+
@method
|
76 |
+
public unlock(x: bigint) {
|
77 |
+
assert(MyLib.add(1n, 2n) == x, 'incorrect sum')
|
78 |
+
}
|
79 |
+
}
|
80 |
+
|
81 |
+
describe('Test SmartContractLib `Lib`', () => {
|
82 |
+
before(async() => {
|
83 |
+
await TestLib.compile()
|
84 |
+
})
|
85 |
+
|
86 |
+
it('should pass integration test successfully.', () => {
|
87 |
+
let testLib = new TestLib()
|
88 |
+
let result = testLib.verify(self => self.unlock(3n))
|
89 |
+
expect(result.success, result.error).to.be.true
|
90 |
+
}
|
91 |
+
})
|
92 |
+
|
93 |
+
```
|
94 |
+
|
95 |
+
## Create and Publish a Library Project Using sCrypt CLI
|
96 |
+
|
97 |
+
The following command will create a demo scryptTS library along with tests and scaffolding:
|
98 |
+
|
99 |
+
```sh
|
100 |
+
scrypt project --lib <your-lib-name>
|
101 |
+
```
|
102 |
+
|
103 |
+
Note the `lib` option is turned on.
|
104 |
+
|
105 |
+
You can publish the library on [NPM](https://www.npmjs.com/) by running the following command in the project's root directory:
|
106 |
+
|
107 |
+
```sh
|
108 |
+
npm publish
|
109 |
+
```
|
110 |
+
|
111 |
+
This will build the project and publish it on NPM. After the library is published, users can simply import it in any other project just like regular NPM packages.
|
112 |
+
|
113 |
+
:::note
|
114 |
+
Named imports are not supported yet. You should only import like the following.
|
115 |
+
:::
|
116 |
+
|
117 |
+
```ts
|
118 |
+
import { MyLib } from βmy_packageβ
|
119 |
+
```
|
120 |
+
|
121 |
+
### Advanced
|
122 |
+
|
123 |
+
For the import system working properly, you should always publish the auto-generated sCrypt contracts (including `scrypt.index.json` file) along with the javascript outputs. The structure of the package could be like this:
|
124 |
+
|
125 |
+
```
|
126 |
+
node_modules
|
127 |
+
|__ my_package
|
128 |
+
|__ dist
|
129 |
+
|__ myLib.js
|
130 |
+
|__ myLib.d.ts
|
131 |
+
|__ artifacts
|
132 |
+
|__ myLib.scrypt
|
133 |
+
|__ scrypt.index.json
|
134 |
+
β¦
|
135 |
+
```
|
136 |
+
|
137 |
+
The `scrypt.index.json` file will be generated at TypeScript compile time in the same directory of your `tsconfig.json` which should be placed in the root folder. It shall not be moved or modified manually. The folder for auto-generated `.scrypt` files (`artifacts` in the upper file tree) can be changed by configuring the `outDir` option in `tsconfig.json`, like:
|
138 |
+
|
139 |
+
```json
|
140 |
+
"compilerOptions": {
|
141 |
+
"plugins": [
|
142 |
+
{
|
143 |
+
"transform": "scrypt-ts/dist/transformation/transformer",
|
144 |
+
"transformProgram": "true",
|
145 |
+
"outDir": "my_scrypts_dir"
|
146 |
+
}
|
147 |
+
]
|
148 |
+
}
|
149 |
+
```
|
150 |
+
|
151 |
+
You should always publish the auto-generated sCrypt files along with the package. If you are familiar with sCrypt development and want to apply some improvements to the auto-generated files, for example using an inline asm function to replace an ordinary function to reduce the final script size, you could just modify the auto-generated file as you wish before publishing it. Take a look at how [scrytp-ts-lib](https://github.com/sCrypt-Inc/scrypt-ts-lib/tree/master/optimizations) does it.
|
152 |
+
|
153 |
+
:::note
|
154 |
+
You should modify the auto-generated files with caution and make sure that the modification passes the tests.
|
155 |
+
:::
|
156 |
+
|
157 |
+
## Related Tools
|
158 |
+
|
159 |
+
### `scrypt-ts-lib`
|
160 |
+
|
161 |
+
Itβs a collection of smart contract libraries provided by us. You can find some useful tools [here](https://github.com/sCrypt-Inc/scrypt-ts-lib). Also you are welcome to contribute.
|
how-to-test-a-contract.md
ADDED
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 5
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Test a Contract
|
6 |
+
|
7 |
+
Before using a smart contract in production, one should always test it carefully, especially because any bug in it may cause **real economic losses**.
|
8 |
+
|
9 |
+
There are two different kinds of tests recommended for every project using `sCrypt`:
|
10 |
+
|
11 |
+
* **Local Unit Testing**
|
12 |
+
* **Testnet Integration Testing**
|
13 |
+
|
14 |
+
Now we will take a look at the file `tests/local/demo.ts`. This file contains code for deployment of our `Demo` contract on the Bitcoin testnet and a subsequent public method call on the contract.
|
15 |
+
|
16 |
+
But before going into details, you should learn some basic models of sCrypt for signing and sending transactions.
|
17 |
+
|
18 |
+
## Compile the Contract
|
19 |
+
|
20 |
+
First, call function `SmartContract.compile()` to compile the contract before doing any testing.
|
21 |
+
|
22 |
+
```ts
|
23 |
+
await Demo.compile()
|
24 |
+
```
|
25 |
+
|
26 |
+
## Provider
|
27 |
+
|
28 |
+
A `Provider` is an abstraction of a standard Bitcoin node that provides connection to the Bitcoin network, for read and write access to the blockchain.
|
29 |
+
|
30 |
+
sCrypt already has a few built-in providers:
|
31 |
+
|
32 |
+
* `DummyProvider`: A mockup provider just for local tests. It does not connect to the Bitcoin blockchain and thus cannot send transactions.
|
33 |
+
|
34 |
+
* `DefaultProvider`: The default provider is the safest, easiest way to begin developing on Bitcoin, and it is also robust enough for use in production. It can be used in testnet as well as mainnet.
|
35 |
+
|
36 |
+
* See full list of providers [here](./reference/classes/Provider.md#hierarchy).
|
37 |
+
|
38 |
+
You can initialize these providers like this:
|
39 |
+
|
40 |
+
```ts
|
41 |
+
let dummyProvider = new DummyProvider();
|
42 |
+
|
43 |
+
// Mainnet
|
44 |
+
let provider = new DefaultProvider();
|
45 |
+
// Or explicitly: let provider = new DefaultProvider(bsv.Networks.mainnet);
|
46 |
+
|
47 |
+
// Testnet
|
48 |
+
let provider = new DefaultProvider(bsv.Networks.testnet);
|
49 |
+
```
|
50 |
+
|
51 |
+
## Signer
|
52 |
+
|
53 |
+
A `Signer` is an abstraction of private keys, which can be used to sign messages and transactions. A simple signer would be a single private key, while a complex signer is a wallet.
|
54 |
+
|
55 |
+
### TestWallet
|
56 |
+
|
57 |
+
For testing purposes only, we have a built-in wallet called `TestWallet`. It can be created like this:
|
58 |
+
|
59 |
+
```ts
|
60 |
+
const signer = new TestWallet(privateKey, provider);
|
61 |
+
```
|
62 |
+
|
63 |
+
`privateKey` can be a single private key or an array of private keys that the wallet can use to sign transactions. The ability of the wallet to send transactions is assigned to `provider`. In other words, a `TestWallet` serves as both a signer and a provider.
|
64 |
+
|
65 |
+
## Test a Contract Locally
|
66 |
+
|
67 |
+
Compared to other blockchains, smart contracts on Bitcoin are **pure**.
|
68 |
+
|
69 |
+
* Given the same input, its public method always returns the same boolean output: success or failure. It has no internal state.
|
70 |
+
* A public method call causes no side effects.
|
71 |
+
|
72 |
+
Smart contracts are similar to mathematical functions. Thus, we can test a contract locally without touching the Bitcoin blockchain. If it passes tests off chain, we are confident it will behave the same on chain.
|
73 |
+
|
74 |
+
### Prepare a Signer and Provider
|
75 |
+
|
76 |
+
For local testing, we can use the `TestWallet`, with a mock provider. The `TestWallet` and `DummyProvider` combination would be ideal for local tests because it can sign the contract call transactions without actually sending them.
|
77 |
+
|
78 |
+
Such a signer may be declared as below:
|
79 |
+
|
80 |
+
```ts
|
81 |
+
let signer = new TestWallet(privateKey, new DummyProvider());
|
82 |
+
```
|
83 |
+
|
84 |
+
Don't forget to connect the signer to the contract instance as well:
|
85 |
+
|
86 |
+
```ts
|
87 |
+
await instance.connect(signer);
|
88 |
+
```
|
89 |
+
|
90 |
+
### Call a Public Method
|
91 |
+
|
92 |
+
Similar to what we described in [this section](../how-to-test-a-contract#call-a-public-method), you can call a contract's public `@method` on the blockchain as follows:
|
93 |
+
|
94 |
+
```ts
|
95 |
+
// build and send tx for calling `foo`
|
96 |
+
const { tx, atInputIndex } = await instance.methods.foo(arg1, arg2, options);
|
97 |
+
console.log(`Smart contract method successfully called with txid ${tx.id}`);
|
98 |
+
```
|
99 |
+
|
100 |
+
Remember that the tx is not actually sent anywhere in a local test because we connect to a mock provider.
|
101 |
+
|
102 |
+
### Verify the Tx input for the method call
|
103 |
+
|
104 |
+
In the previous step, the signed `tx` for the contract call and its input index are returned. You can call `verifyScript` on the returned `tx` to verify that the contract method call at the given tx input index is successful.
|
105 |
+
|
106 |
+
```ts
|
107 |
+
let result = tx.verifyScript(atInputIndex)
|
108 |
+
console.log(result.success) // Output: true or false
|
109 |
+
```
|
110 |
+
|
111 |
+
### Integrate with a testing framework
|
112 |
+
|
113 |
+
You can use whatever testing framework you like to write unit tests for your contract. For example, a local test using [Mocha](https://mochajs.org/) is shown below:
|
114 |
+
|
115 |
+
```js
|
116 |
+
describe('Test SmartContract `Demo`', () => {
|
117 |
+
let signer;
|
118 |
+
let demo;
|
119 |
+
|
120 |
+
before(async () => {
|
121 |
+
// compile contract
|
122 |
+
await Demo.compile()
|
123 |
+
|
124 |
+
// create a test wallet as signer, connected to a dummy provider
|
125 |
+
signer = new TestWallet(privateKey, new DummyProvider())
|
126 |
+
|
127 |
+
// initialize a contract instance
|
128 |
+
demo = new Demo(1n, 2n)
|
129 |
+
|
130 |
+
// connect the instance to signer
|
131 |
+
await demo.connect(signer)
|
132 |
+
})
|
133 |
+
|
134 |
+
it('should pass the public method unit test successfully.', async () => {
|
135 |
+
// call `demo.methods.add` to get a signed tx
|
136 |
+
const { tx: callTx, atInputIndex } = await demo.methods.add(
|
137 |
+
// pass in the right argument
|
138 |
+
3n,
|
139 |
+
// set method call options
|
140 |
+
{
|
141 |
+
// Since `demo.deploy` hasn't been called before, a fake UTXO of the contract should be passed in.
|
142 |
+
fromUTXO: dummyUTXO
|
143 |
+
} as MethodCallOptions<Demo>
|
144 |
+
)
|
145 |
+
|
146 |
+
let result = callTx.verifyScript(atInputIndex)
|
147 |
+
expect(result.success, result.error).to.eq(true)
|
148 |
+
})
|
149 |
+
|
150 |
+
it('should pass the non-public method unit test', () => {
|
151 |
+
expect(demo.sum(3n, 4n)).to.be.eq(7n)
|
152 |
+
})
|
153 |
+
|
154 |
+
it('should throw error', () => {
|
155 |
+
return expect(
|
156 |
+
// Using the wrong argument when calling this function just results in an error.
|
157 |
+
demo.methods.add(4n, { fromUTXO: dummyUTXO })
|
158 |
+
).to.be.rejectedWith(/add check failed/)
|
159 |
+
})
|
160 |
+
})
|
161 |
+
```
|
162 |
+
|
163 |
+
## Test a Stateful Contract
|
164 |
+
|
165 |
+
Stateful contact testing is very similar to what we have described above. The only different is that you have to be aware of smart contract instance changes after method calls.
|
166 |
+
|
167 |
+
As described in the [Overview](./how-to-write-a-contract/stateful-contract.md#overview), for each method call, a tx contains new contract UTXO(s) with the latest updated state, i.e., the next instance. From the perspective of the current spending tx, the public `@method` of a contract instance is called in one of its inputs, and the next contract instance is stored in one (or more) of its outputs.
|
168 |
+
|
169 |
+
Now, let's look at how to test the `incrementOnChain` method call:
|
170 |
+
|
171 |
+
```ts
|
172 |
+
// initialize the first instance, i.e., deployment
|
173 |
+
let counter = new Counter(0n);
|
174 |
+
// connect it to a signer
|
175 |
+
counter.connect(dummySigner());
|
176 |
+
|
177 |
+
// set the current instance to be the first instance
|
178 |
+
let current = counter;
|
179 |
+
|
180 |
+
// create the next instance from the current
|
181 |
+
let nextInstance = current.next();
|
182 |
+
|
183 |
+
// apply the same updates on the next instance locally
|
184 |
+
nextInstance.increment();
|
185 |
+
|
186 |
+
// call the method of current instance to apply the updates on chain
|
187 |
+
const { tx: tx_i, atInputIndex } = await current.methods.incrementOnChain(
|
188 |
+
{
|
189 |
+
// Since `counter.deploy` hasn't been called before, a fake UTXO of the contract should be passed in.
|
190 |
+
fromUTXO: getDummyUTXO(balance),
|
191 |
+
|
192 |
+
// the `next` instance and its balance should be provided here
|
193 |
+
next: {
|
194 |
+
instance: nextInstance,
|
195 |
+
balance
|
196 |
+
}
|
197 |
+
} as MethodCallOptions<Counter>
|
198 |
+
);
|
199 |
+
|
200 |
+
// check the validity of the input script generated for the method call.
|
201 |
+
let result = tx_i.verifyScript(atInputIndex);
|
202 |
+
expect(result.success, result.error).to.eq(true);
|
203 |
+
|
204 |
+
```
|
205 |
+
|
206 |
+
In general, we call the method of a stateful contract in 3 steps:
|
207 |
+
|
208 |
+
### 1. Build the `current` instance
|
209 |
+
|
210 |
+
The `current` instance refers to the contract instance containing the latest state on the blockchain. The first instance is in the deployment transaction. In the above example, we initialize the `current` instance to be the first instance like this:
|
211 |
+
|
212 |
+
```ts
|
213 |
+
let current = counter;
|
214 |
+
```
|
215 |
+
|
216 |
+
### 2. Create a `next` instance and apply updates to it off chain
|
217 |
+
|
218 |
+
The `next` instance is the new instance in the UTXO of the method calling tx.
|
219 |
+
|
220 |
+
To create the `next` of a specific contract instance, you can simply call `next()` on it:
|
221 |
+
|
222 |
+
```ts
|
223 |
+
let nextInstance = instance.next();
|
224 |
+
```
|
225 |
+
|
226 |
+
It will make a deep copy of all properties and methods of `instance` to create a new one.
|
227 |
+
|
228 |
+
Then, you should apply all the state updates to the `next` instance. Please note that these are just local/off-chain updates and are yet to be applied to the blockchain.
|
229 |
+
|
230 |
+
```ts
|
231 |
+
nextInstance.increment();
|
232 |
+
```
|
233 |
+
This is the **SAME** method we call on chain in `incrementOnChain`, thanks to the fact that both the on-chain smart contract and off-chain code are written in TypeScript.
|
234 |
+
|
235 |
+
### 3. Call the method on the `current` instance to apply updates on chain
|
236 |
+
|
237 |
+
As described in [this section](#call-a-public-method), we can build a call transaction. The only difference here is that we pass in the `next` instance and its balance as a method call option in a stateful contract. So the method (i.e., `incrementOnChain`) have all the information to verify that all updates made to the `next` instance follow the state transition rules in it.
|
238 |
+
|
239 |
+
```ts
|
240 |
+
const { tx: tx_i, atInputIndex } = await current.methods.incrementOnChain(
|
241 |
+
{
|
242 |
+
// Since `counter.deploy` hasn't been called before, a fake UTXO of the contract should be passed in.
|
243 |
+
fromUTXO: getDummyUTXO(balance),
|
244 |
+
|
245 |
+
// the `next` instance and its balance should be provided here
|
246 |
+
next: {
|
247 |
+
instance: nextInstance,
|
248 |
+
balance
|
249 |
+
}
|
250 |
+
} as MethodCallOptions<Counter>
|
251 |
+
);
|
252 |
+
```
|
253 |
+
|
254 |
+
Finally, we can check the validity of the method call as before.
|
255 |
+
|
256 |
+
```ts
|
257 |
+
let result = tx_i.verifyScript(atInputIndex);
|
258 |
+
expect(result.success, result.error).to.eq(true);
|
259 |
+
```
|
260 |
+
|
261 |
+
### Running the tests
|
262 |
+
|
263 |
+
As before, we can just use the following command:
|
264 |
+
|
265 |
+
```sh
|
266 |
+
npm run test
|
267 |
+
```
|
268 |
+
Full code is [here](https://github.com/sCrypt-Inc/boilerplate/blob/master/tests/local/counter.test.ts).
|
269 |
+
|
270 |
+
You may visit [here](./how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md) to see more details on contract deployment and call.
|
how-to-verify-a-contract.md
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 11
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Verify a Contract
|
6 |
+
|
7 |
+
You will learn how to verify smart contracts on [WhatsOnChain](https://whatsonchain.com/) (WoC), a blockchain explorer.
|
8 |
+
By verifying your smart contract on WoC, anyone can view its source code and interact with it confidently. Let's get started!
|
9 |
+
|
10 |
+
|
11 |
+
To start with the verification process, we need to first deploy a smart contract. Let us use the ["Hello World" tutorial](./tutorials/hello-world.md) as an example. After you complete the tutorial, you should get the ID of the deployment transaction such as [`a34d4e45a9108b5b9da4faf4f086e9ef36b79466383bd7a22ff2c7f6a562546c`](https://test.whatsonchain.com/tx/a34d4e45a9108b5b9da4faf4f086e9ef36b79466383bd7a22ff2c7f6a562546c).
|
12 |
+
|
13 |
+
|
14 |
+
If you take a look at the transaction on WoC, you'll see that the first output contains a script identified by the hash `eb2f10b8f1bd12527f07a5d05b40f06137cbebe4e9ecfb6a4e0fd8a3437e1def`, which contains your contract in script format.
|
15 |
+
|
16 |
+

|
17 |
+
|
18 |
+
This hash is referred to as the `scriptHash`. It's essentially just a `sha256` hash value of the deployed contracts locking script, encoded in a little-endian hex format. It is commonly used as an index by block explorers. You can also get this value locally, via the contract instance's `scriptHash` property:
|
19 |
+
|
20 |
+
```ts
|
21 |
+
console.log(instance.scriptHash)
|
22 |
+
// eb2f10b8f1bd12527f07a5d05b40f06137cbebe4e9ecfb6a4e0fd8a3437e1def
|
23 |
+
```
|
24 |
+
|
25 |
+
:::note
|
26 |
+
The scriptHash value can vary due to factors like the current property values and the number of times the contract has been updated, leading to inconsistencies in its value.
|
27 |
+
:::
|
28 |
+
|
29 |
+
You can submit and verify sCrypt source code that belongs to a specific script hash.
|
30 |
+
|
31 |
+

|
32 |
+
|
33 |
+
There are two ways to verify it.
|
34 |
+
|
35 |
+
## 1. Using WOC sCrypt Plugin
|
36 |
+
|
37 |
+
At the deployed transaction on WOC, click on the `ScriptHash` of the first output. It will open a page like this:
|
38 |
+
|
39 |
+

|
40 |
+
|
41 |
+
You shall see an `sCrypt` tab. Click on it. You'll see a very simple form:
|
42 |
+
|
43 |
+
|
44 |
+

|
45 |
+
|
46 |
+
In the form you are able to select the version of sCrypt you've used to compile and deploy the contract, along with a text-box in which you need to paste the source code.
|
47 |
+
|
48 |
+
|
49 |
+

|
50 |
+
|
51 |
+
|
52 |
+
Now click `Submit`. If the code is correct, you should see something like the following in a few seconds:
|
53 |
+
|
54 |
+
|
55 |
+

|
56 |
+
|
57 |
+
Congrats, you have verified your first smart contract!
|
58 |
+
|
59 |
+
Now, every time someone opens the `sCrypt` tab on [the script hash page](https://test.whatsonchain.com/script/eb2f10b8f1bd12527f07a5d05b40f06137cbebe4e9ecfb6a4e0fd8a3437e1def), they will see the verified smart contract source code, as well as its constructor parameters when deployed.
|
60 |
+
|
61 |
+
## 2. Using CLI
|
62 |
+
|
63 |
+
The same process can be done using the [sCrypt CLI](https://www.npmjs.com/package/scrypt-cli).
|
64 |
+
You can verify the deployed smart contracts script using the `verify` command:
|
65 |
+
|
66 |
+
```sh
|
67 |
+
scrypt verify <scriptHash> <contractPath>
|
68 |
+
```
|
69 |
+
|
70 |
+
The first positional argument is the script hash of the deployed contract and the second one is the path to the file which contains the sCrypt smart contract. Note, that the file must also include all the code it depends on, i.e. third party libraries.
|
71 |
+
|
72 |
+
Using the `network` option, you can specify on which network the contract is deployed. This defaults to `test`, indicating the Bitcoin testnet:
|
73 |
+
|
74 |
+
```sh
|
75 |
+
scrypt verify --network main <scriptHash> <contractPath>
|
76 |
+
```
|
77 |
+
|
78 |
+
You can also specify the version of sCrypt used during verification. By default, the command will use the version specified in `package.json`:
|
79 |
+
|
80 |
+
```sh
|
81 |
+
scrypt verify -V 0.2.0-beta.9 <scriptHash> <contractPath>
|
82 |
+
```
|
83 |
+
|
84 |
+
For example, if we would like to verify the same deployed contract as above, we would simply run the following:
|
85 |
+
|
86 |
+
```sh
|
87 |
+
scrypt verify eb2f10b8f1bd12527f07a5d05b40f06137cbebe4e9ecfb6a4e0fd8a3437e1def src/contracts/demoproject.ts
|
88 |
+
```
|
89 |
+
|
90 |
+
Upon execution, the designated contract code undergoes verification on sCrypt's servers. If successful, the outcome will be [displayed on WoC](https://test.whatsonchain.com/script/eb2f10b8f1bd12527f07a5d05b40f06137cbebe4e9ecfb6a4e0fd8a3437e1def), under the "sCrypt" tab, just like above.
|
91 |
+
|
how-to-write-a-contract.md
ADDED
@@ -0,0 +1,658 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 1
|
3 |
+
---
|
4 |
+
|
5 |
+
# How to Write a Contract
|
6 |
+
|
7 |
+
A smart contract is a class that extends the `SmartContract` base class. A simple example is shown below.
|
8 |
+
|
9 |
+
```ts
|
10 |
+
import { SmartContract, method, prop, assert } from "scrypt-ts"
|
11 |
+
|
12 |
+
class Demo extends SmartContract {
|
13 |
+
@prop()
|
14 |
+
readonly x: bigint
|
15 |
+
|
16 |
+
constructor(x: bigint) {
|
17 |
+
super(...arguments)
|
18 |
+
this.x = x
|
19 |
+
}
|
20 |
+
|
21 |
+
@method()
|
22 |
+
public unlock(x: bigint) {
|
23 |
+
assert(this.add(this.x, 1n) == x, 'incorrect sum')
|
24 |
+
}
|
25 |
+
|
26 |
+
@method()
|
27 |
+
add(x0: bigint, x1:bigint) : bigint {
|
28 |
+
return x0 + x1
|
29 |
+
}
|
30 |
+
}
|
31 |
+
```
|
32 |
+
|
33 |
+
Class members decorated with `@prop` and `@method` will end up on the blockchain and thus must be a strict subset of TypeScript. Everywhere decorated with them can be regarded in the on-chain context. Members decorated with neither are regular TypeScript and are kept off chain. The significant benefit of `sCrypt` is that both on-chain and off-chain code are written in the same language: TypeScript.
|
34 |
+
|
35 |
+
:::note
|
36 |
+
You can use [the sCrypt template Repl](https://replit.com/@msinkec/sCrypt) and play with the code in your browser!
|
37 |
+
:::
|
38 |
+
|
39 |
+
## Properties
|
40 |
+
|
41 |
+
A smart contract can have two kinds of properties:
|
42 |
+
|
43 |
+
1. With `@prop` decorator: these properties are **only allowed to have [types](#data-types) specified below** and they shall only be initialized in the constructor.
|
44 |
+
|
45 |
+
2. Without `@prop` decorator: these properties are regular TypeScript properties without any special requirement, meaning they can use any types. Accessing these properties is prohibited in methods decorated with the `@method` decorator.
|
46 |
+
|
47 |
+
|
48 |
+
### `@prop` decorator
|
49 |
+
|
50 |
+
Use this decorator to mark any property that intends to be stored on chain.
|
51 |
+
|
52 |
+
This decorator takes a `boolean` parameter. By default, it is set to `false`, meaning the property cannot be changed after the contract is deployed. If the value is `true`, the property is a so-called [stateful](./stateful-contract) property and its value can be updated in subsequent contract calls.
|
53 |
+
|
54 |
+
```ts
|
55 |
+
// good, `a` is stored on chain, and it's readonly after the contract is deployed
|
56 |
+
@prop()
|
57 |
+
readonly a: bigint
|
58 |
+
|
59 |
+
// valid, but not good enough, `a` cannot be changed after the contract is deployed
|
60 |
+
@prop()
|
61 |
+
a: bigint
|
62 |
+
|
63 |
+
// good, `b` is stored on chain, and its value can be updated in subsequent contract calls
|
64 |
+
@prop(true)
|
65 |
+
b: bigint
|
66 |
+
|
67 |
+
// invalid, `b` is a stateful property that cannot be readonly
|
68 |
+
@prop(true)
|
69 |
+
readonly b: bigint
|
70 |
+
|
71 |
+
// good
|
72 |
+
@prop()
|
73 |
+
static c: bigint = 1n
|
74 |
+
|
75 |
+
// invalid, static property must be initialized when declared
|
76 |
+
@prop()
|
77 |
+
static c: bigint
|
78 |
+
|
79 |
+
// invalid, stateful property cannot be static
|
80 |
+
@prop(true)
|
81 |
+
static c: bigint = 1n
|
82 |
+
|
83 |
+
// good, `UINT_MAX` is a compile-time constant, and no need to typed explicitly
|
84 |
+
static readonly UINT_MAX = 0xffffffffn
|
85 |
+
|
86 |
+
// valid, but not good enough, `@prop()` is not necessary for the CTC
|
87 |
+
@prop()
|
88 |
+
static readonly UINT_MAX = 0xffffffffn
|
89 |
+
|
90 |
+
// invalid
|
91 |
+
@prop(true)
|
92 |
+
static readonly UINT_MAX = 0xffffffffn
|
93 |
+
```
|
94 |
+
|
95 |
+
## Constructor
|
96 |
+
|
97 |
+
A smart contract must have an explicit constructor if it has at least one `@prop` that is not `static`.
|
98 |
+
|
99 |
+
The `super` method **must** be called in the constructor and all the arguments of the constructor should be passed to `super`
|
100 |
+
in the same order as they are passed into the constructor. For example,
|
101 |
+
|
102 |
+
```ts
|
103 |
+
class A extends SmartContract {
|
104 |
+
readonly p0: bigint
|
105 |
+
|
106 |
+
@prop()
|
107 |
+
readonly p1: bigint
|
108 |
+
|
109 |
+
@prop()
|
110 |
+
readonly p2: boolean
|
111 |
+
|
112 |
+
constructor(p0: bigint, p1: bigint, p2: boolean) {
|
113 |
+
super(...arguments) // same as super(p0, p1, p2)
|
114 |
+
this.p0 = p0
|
115 |
+
this.p1 = p1
|
116 |
+
this.p2 = p2
|
117 |
+
}
|
118 |
+
}
|
119 |
+
```
|
120 |
+
[`arguments`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments) is an array containing the values of the arguments passed to that function. `...` is the [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax).
|
121 |
+
|
122 |
+
|
123 |
+
## Methods
|
124 |
+
|
125 |
+
Like properties, a smart contract can also have two kinds of methods:
|
126 |
+
|
127 |
+
1. With `@method` decorator: these methods can only call **methods also decorated by `@method` or [functions](#functions) specified below**. Also, **only the properties decorated by `@prop` can be accessed**.
|
128 |
+
|
129 |
+
2. Without `@method` decorator: these methods are just regular TypeScript class methods.
|
130 |
+
|
131 |
+
|
132 |
+
### `@method` decorator
|
133 |
+
|
134 |
+
1. Use this decorator to mark any method that intends to run on chain.
|
135 |
+
2. It takes a [sighash flag](./scriptcontext.md#sighash-type) as a parameter.
|
136 |
+
|
137 |
+
|
138 |
+
### Public `@method`s
|
139 |
+
|
140 |
+
Each contract **must** have at least one public `@method`. It is denoted with the `public` modifier and does not return any value. It is visible outside the contract and acts as the main method into the contract (like `main` in C and Java).
|
141 |
+
|
142 |
+
A public `@method` can be called from an external transaction. The call succeeds if it runs to completion without violating any conditions in [assert()](./built-ins.md#assert). An example is shown below.
|
143 |
+
|
144 |
+
```ts
|
145 |
+
@method()
|
146 |
+
public unlock(x: bigint) {
|
147 |
+
// only succeeds if x is 1
|
148 |
+
assert(this.add(this.x, 1n) == x, "unequal")
|
149 |
+
}
|
150 |
+
```
|
151 |
+
|
152 |
+
:::note
|
153 |
+
The last function call of a public `@method` method **must** be an `assert()` function call, unless it is a `console.log()` call.
|
154 |
+
:::
|
155 |
+
|
156 |
+
```ts
|
157 |
+
class PublicMethodDemo extends SmartContract {
|
158 |
+
@method()
|
159 |
+
public foo() {
|
160 |
+
// invalid, the last statement of public method should be an `assert` function call
|
161 |
+
}
|
162 |
+
|
163 |
+
@method()
|
164 |
+
public bar() {
|
165 |
+
assert(true);
|
166 |
+
return 1n; // invalid, because a public method cannot return any value
|
167 |
+
}
|
168 |
+
|
169 |
+
@method()
|
170 |
+
public foobar() {
|
171 |
+
console.log();
|
172 |
+
// valid, `console.log` calling will be ignored when verifying the last `assert` statement
|
173 |
+
assert(true);
|
174 |
+
console.log();
|
175 |
+
console.log();
|
176 |
+
}
|
177 |
+
}
|
178 |
+
```
|
179 |
+
|
180 |
+
### Non-public `@method`s
|
181 |
+
|
182 |
+
Without a `public` modifier, a `@method` is internal and cannot be directly called from an external transaction.
|
183 |
+
|
184 |
+
```ts
|
185 |
+
@method()
|
186 |
+
add(x0: bigint, x1:bigint) : bigint {
|
187 |
+
return x0 + x1
|
188 |
+
}
|
189 |
+
```
|
190 |
+
|
191 |
+
:::note
|
192 |
+
**Recursion is disallowed**. A `@method`, public and not, cannot call itself, either directly in its own body or indirectly calls another method that transitively calls itself.
|
193 |
+
:::
|
194 |
+
|
195 |
+
```ts
|
196 |
+
class MethodsDemo extends SmartContract {
|
197 |
+
@prop()
|
198 |
+
readonly x: bigint;
|
199 |
+
@prop()
|
200 |
+
readonly y: bigint;
|
201 |
+
|
202 |
+
constructor(x: bigint, y: bigint) {
|
203 |
+
super(...arguments);
|
204 |
+
this.x = x;
|
205 |
+
this.y = y;
|
206 |
+
}
|
207 |
+
|
208 |
+
// good, non-public static method without access `@prop` properties
|
209 |
+
@method()
|
210 |
+
static sum(a: bigint, b: bigint): bigint {
|
211 |
+
return a + b;
|
212 |
+
}
|
213 |
+
|
214 |
+
// good, non-public method
|
215 |
+
@method()
|
216 |
+
xyDiff(): bigint {
|
217 |
+
return this.x - this.y
|
218 |
+
}
|
219 |
+
|
220 |
+
// good, public method
|
221 |
+
@method()
|
222 |
+
public add(z: bigint) {
|
223 |
+
// good, call `sum` with the class name
|
224 |
+
assert(z == MethodsDemo.sum(this.x, this.y), 'add check failed');
|
225 |
+
}
|
226 |
+
|
227 |
+
// good, another public method
|
228 |
+
@method()
|
229 |
+
public sub(z: bigint) {
|
230 |
+
// good, call `xyDiff` with the class instance
|
231 |
+
assert(z == this.xyDiff(), 'sub check failed');
|
232 |
+
}
|
233 |
+
|
234 |
+
// valid but bad, public static method
|
235 |
+
@method()
|
236 |
+
public static alwaysPass() {
|
237 |
+
assert(true)
|
238 |
+
}
|
239 |
+
}
|
240 |
+
```
|
241 |
+
|
242 |
+
## Data Types
|
243 |
+
|
244 |
+
Types used in `@prop` and `@method` are restricted to these kinds:
|
245 |
+
|
246 |
+
### Basic Types
|
247 |
+
|
248 |
+
#### boolean
|
249 |
+
|
250 |
+
A simple value `true` or `false`.
|
251 |
+
```ts
|
252 |
+
let isDone: boolean = false
|
253 |
+
```
|
254 |
+
|
255 |
+
#### `bigint`
|
256 |
+
|
257 |
+
`bigint` can represent arbitrarily large integers. A [bigint literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) is a number with suffix `n`:
|
258 |
+
|
259 |
+
```ts
|
260 |
+
11n
|
261 |
+
0x33FEn
|
262 |
+
const previouslyMaxSafeInteger = 9007199254740991n
|
263 |
+
const alsoHuge = BigInt(9007199254740991)
|
264 |
+
// 9007199254740991n
|
265 |
+
const hugeHex: bigint = BigInt("0x1fffffffffffff")
|
266 |
+
// 9007199254740991n
|
267 |
+
```
|
268 |
+
|
269 |
+
#### `ByteString`
|
270 |
+
|
271 |
+
In a smart contract context (i.e., in `@method`s or `@prop`s), a `ByteString` represents a byte array.
|
272 |
+
|
273 |
+
A literal `string` can be converted in to a `ByteString` using function `toByteString(literal: string, isUtf8: boolean = false): ByteString`:
|
274 |
+
|
275 |
+
* If not passing `isUtf8` or `isUtf8` is `false`, then `literal` should be in the format of hex literal, which can be represented by the regular expression: `/^([0-9a-fA-F]{2})*$/`
|
276 |
+
* Otherwise, `literal` should be in the format of utf8 literal, e.g., `hello world`.
|
277 |
+
|
278 |
+
:::note
|
279 |
+
`toByteString` **ONLY** accepts string literals for its first argument, and boolean literals for the second.
|
280 |
+
:::
|
281 |
+
|
282 |
+
```ts
|
283 |
+
let a = toByteString('0011') // valid, `0011` is a valid hex literal
|
284 |
+
// 0011
|
285 |
+
let b = toByteString('hello world', true) // valid
|
286 |
+
// 68656c6c6f20776f726c64
|
287 |
+
|
288 |
+
toByteString('0011', false) // valid
|
289 |
+
// 30303131
|
290 |
+
|
291 |
+
toByteString(b, true) // invalid, not passing string literal to the 1st parameter
|
292 |
+
|
293 |
+
toByteString('001') // invalid, `001` is not a valid hex literal
|
294 |
+
toByteString('hello', false) // invalid, `hello` is not a valid hex literal
|
295 |
+
|
296 |
+
toByteString('hello', 1 === 1) // invalid, not passing boolean literal to the 2nd parameter
|
297 |
+
|
298 |
+
let c = true
|
299 |
+
toByteString('world', c) // invalid, not passing boolean literal to the 2nd parameter
|
300 |
+
```
|
301 |
+
|
302 |
+
`ByteString` has the following operators and methods:
|
303 |
+
|
304 |
+
* `==` / `===`: compare
|
305 |
+
|
306 |
+
* `+`: concatenate
|
307 |
+
|
308 |
+
```ts
|
309 |
+
const str0 = toByteString('01ab23ef68')
|
310 |
+
const str1 = toByteString('656c6c6f20776f726c64')
|
311 |
+
|
312 |
+
// comparison
|
313 |
+
str0 == str1
|
314 |
+
str0 === str1
|
315 |
+
// false
|
316 |
+
|
317 |
+
// concatenation
|
318 |
+
str0 + str1
|
319 |
+
// '01ab23ef68656c6c6f20776f726c64'
|
320 |
+
```
|
321 |
+
|
322 |
+
#### `number`
|
323 |
+
|
324 |
+
Type `number` is not allowed in `@prop`s and `@method`s, except in the following cases. We can use `Number()` function to convert `bigint` to `number`.
|
325 |
+
|
326 |
+
* Array index
|
327 |
+
|
328 |
+
```ts
|
329 |
+
let arr: FixedArray<bigint, 3> = [1n, 3n, 3n]
|
330 |
+
let idx: bigint = 2n
|
331 |
+
let item = arr[Number(idx)]
|
332 |
+
```
|
333 |
+
|
334 |
+
* Loop variable
|
335 |
+
|
336 |
+
``` ts
|
337 |
+
for (let i: number = 0 i < 10 i++) {
|
338 |
+
let j: bigint = BigInt(i) // convert number to bigint
|
339 |
+
}
|
340 |
+
```
|
341 |
+
|
342 |
+
It can also be used in defining [compile-time constants](#compile-time-constant).
|
343 |
+
|
344 |
+
|
345 |
+
### Fixed Size Array
|
346 |
+
|
347 |
+
All arrays **must** be of fixed size and be declared as type of `FixedArray<T, SIZE>`, whose `SIZE` must be a [CTC](#compile-time-constant) described later.
|
348 |
+
The common TypeScript arrays declared as `T[]` or `Array<T>` are not allowed in `@prop`s and `@method`s, as they are of dynamic size.
|
349 |
+
|
350 |
+
```ts
|
351 |
+
let aaa: FixedArray<bigint, 3> = [1n, 3n, 3n]
|
352 |
+
|
353 |
+
// set to all 0s
|
354 |
+
const N = 20
|
355 |
+
let aab: FixedArray<bigint, N> = fill(0n, N)
|
356 |
+
|
357 |
+
// 2-dimensional array
|
358 |
+
let abb: FixedArray<FixedArray<bigint, 2>, 3> = [[1n, 3n], [1n, 3n], [1n, 3n]]
|
359 |
+
```
|
360 |
+
|
361 |
+
:::caution
|
362 |
+
A `FixedArray` behaves differently in an on-chain and off-chain context, when passed as a function argument. It is *passed by reference* off chain, as a regular TypeScript/JavaScript array, while *passed by value* on chain. It is thus strongly recommended to NEVER mutate a `FixedArray` parameter's element inside a function.
|
363 |
+
|
364 |
+
```ts
|
365 |
+
class DemoContract extends SmartContract {
|
366 |
+
|
367 |
+
@prop(true)
|
368 |
+
readonly a: FixedArray<bigint, 3>
|
369 |
+
|
370 |
+
constructor(a: FixedArray<bigint, 3>) {
|
371 |
+
super(...arguments)
|
372 |
+
this.a = a
|
373 |
+
}
|
374 |
+
|
375 |
+
@method()
|
376 |
+
onchainChange(a: FixedArray<bigint, 3>) {
|
377 |
+
a[0] = 0
|
378 |
+
}
|
379 |
+
|
380 |
+
offchainChange(a: FixedArray<bigint, 3>) {
|
381 |
+
a[0] = 0
|
382 |
+
}
|
383 |
+
|
384 |
+
@method()
|
385 |
+
public main(a: FixedArray<bigint, 3>) {
|
386 |
+
this.onchainChange(this.a)
|
387 |
+
// note: a[0] is not changed on chain
|
388 |
+
assert(this.a[0] == 1n)
|
389 |
+
}
|
390 |
+
}
|
391 |
+
|
392 |
+
const arrayA: FixedArray<bigint, 3> = [1n, 2n, 3n]
|
393 |
+
const instance = new DemoContract(arrayA);
|
394 |
+
|
395 |
+
instance.offchainChange(arrayA)
|
396 |
+
// note: arrayA[0] is changed off chain
|
397 |
+
assert(arrayA[0] = 0n)
|
398 |
+
```
|
399 |
+
:::
|
400 |
+
|
401 |
+
### User-defined Types
|
402 |
+
|
403 |
+
Users can be define customized types using `type` or `interface`, made of basic types.[^1]
|
404 |
+
|
405 |
+
```ts
|
406 |
+
type ST = {
|
407 |
+
a: bigint
|
408 |
+
b: boolean
|
409 |
+
}
|
410 |
+
|
411 |
+
interface ST1 {
|
412 |
+
x: ST
|
413 |
+
y: ByteString
|
414 |
+
}
|
415 |
+
|
416 |
+
type Point = {
|
417 |
+
x: number
|
418 |
+
y: number
|
419 |
+
}
|
420 |
+
|
421 |
+
function printCoord(pt: Point) {
|
422 |
+
console.log("The coordinate's x value is " + pt.x)
|
423 |
+
console.log("The coordinate's y value is " + pt.y)
|
424 |
+
}
|
425 |
+
|
426 |
+
interface Point2 {
|
427 |
+
x: number
|
428 |
+
y: number
|
429 |
+
}
|
430 |
+
|
431 |
+
// Exactly the same as the earlier example
|
432 |
+
function printCoord(pt: Point2) {
|
433 |
+
console.log("The coordinate's x value is " + pt.x)
|
434 |
+
console.log("The coordinate's y value is " + pt.y)
|
435 |
+
}
|
436 |
+
|
437 |
+
```
|
438 |
+
|
439 |
+
[^1]: A user-defined type is also passed by value on chain, and by reference off chain, same as a `FixedArray`. It is thus strongly recommended to NEVER mutate the field of a parameter, which is of a user-defined type, inside a function.
|
440 |
+
|
441 |
+
### Domain Types
|
442 |
+
|
443 |
+
There are several domain types, specific to the Bitcoin context, used to further improve type safety. They are all subtypes of `ByteString`. That is, they can be used where a `ByteString` is expected, but not vice versa.
|
444 |
+
|
445 |
+
|
446 |
+
* `PubKey` - a public key
|
447 |
+
|
448 |
+
* `Sig` - a signature type in [DER format](https://academy.bit2me.com/en/que-son-firmas-estrictas-der), including sighash flags at the end
|
449 |
+
|
450 |
+
* `Ripemd160` - a RIPEMD-160 hash
|
451 |
+
|
452 |
+
* `PubKeyHash` - an alias for `Ripemd160`, usually representing a bitcoin address.
|
453 |
+
|
454 |
+
* `Sha1` - a SHA-1 hash
|
455 |
+
|
456 |
+
* `Sha256` - a SHA-256 hash
|
457 |
+
|
458 |
+
* `SigHashType` - a sighash
|
459 |
+
|
460 |
+
* `SigHashPreimage` - a sighash preimage
|
461 |
+
|
462 |
+
* `OpCodeType` - a Script [opcode](https://wiki.bitcoinsv.io/index.php/Opcodes_used_in_Bitcoin_Script)
|
463 |
+
|
464 |
+
```ts
|
465 |
+
@method()
|
466 |
+
public unlock(sig: Sig, pubkey: PubKey) {
|
467 |
+
// hash160() takes a ByteString as input, but can accept pubkey here, which if of type PubKey
|
468 |
+
assert(hash160(pubkey) == this.pubKeyHash)
|
469 |
+
assert(this.checkSig(sig, pubkey), 'signature check failed')
|
470 |
+
}
|
471 |
+
```
|
472 |
+
|
473 |
+
## Statements
|
474 |
+
|
475 |
+
There are some constraints on these following statements within `@method`s, except [variable declarations](#Variable-declarations).
|
476 |
+
|
477 |
+
### Variable declarations
|
478 |
+
|
479 |
+
Variables can be declared in `@method`s by keywords `const` / `var` / `let`, like in normal TypeScript.
|
480 |
+
|
481 |
+
```ts
|
482 |
+
let a : bigint = 1n
|
483 |
+
var b: boolean = false
|
484 |
+
const byte: ByteString = toByteString("ff")
|
485 |
+
```
|
486 |
+
|
487 |
+
### `for`
|
488 |
+
|
489 |
+
Bitcoin does not allow unbounded loops for security reasons, to prevent DoS attacks. All loops must be bounded at compile time. So if you want to loop inside `@method`, you must strictly use the following format:
|
490 |
+
|
491 |
+
```ts
|
492 |
+
for (let $i = 0; $i < $maxLoopCount; $i++) {
|
493 |
+
...
|
494 |
+
}
|
495 |
+
```
|
496 |
+
|
497 |
+
:::note
|
498 |
+
* the initial value must be `0` or `0n`, the operator `<` (no `<=`), and increment `$i++` (no pre-increment `++$i`).
|
499 |
+
* `$maxLoopCount` must be a [CTC](#compile-time-constant).
|
500 |
+
* `$i` can be arbitrary name, e.g., `i`, `j`, or `k`. It can be both a `number` or a `bigint` type.
|
501 |
+
* `break` and `continue` are currently not allowed, but can be emulated like
|
502 |
+
:::
|
503 |
+
|
504 |
+
```ts
|
505 |
+
// emulate break
|
506 |
+
let x = 3n
|
507 |
+
let done = false
|
508 |
+
for (let i = 0; i < 3; i++) {
|
509 |
+
if (!done) {
|
510 |
+
x = x * 2n
|
511 |
+
if (x >= 8n) {
|
512 |
+
done = true
|
513 |
+
}
|
514 |
+
}
|
515 |
+
}
|
516 |
+
```
|
517 |
+
|
518 |
+
### `return`
|
519 |
+
|
520 |
+
Due to the lack of native return semantics support in Bitcoin Script, a function currently must end with a `return` statement and it is the only valid place for a `return` statement. This requirement may be relaxed in the future.
|
521 |
+
|
522 |
+
```ts
|
523 |
+
@method() m(x: bigint): bigint {
|
524 |
+
if (x > 2n) return x // invalid
|
525 |
+
return x + 1n // valid
|
526 |
+
}
|
527 |
+
```
|
528 |
+
|
529 |
+
This is usually not a problem since it can be circumvented as follows:
|
530 |
+
```ts
|
531 |
+
@method()
|
532 |
+
abs(a: bigint): bigint {
|
533 |
+
if (a > 0) {
|
534 |
+
return a
|
535 |
+
} else {
|
536 |
+
return -a
|
537 |
+
}
|
538 |
+
}
|
539 |
+
```
|
540 |
+
can be rewritten as
|
541 |
+
```ts
|
542 |
+
@method()
|
543 |
+
abs(a: bigint): bigint {
|
544 |
+
let ret : bigint = 0
|
545 |
+
|
546 |
+
if (a > 0) {
|
547 |
+
ret = a
|
548 |
+
} else {
|
549 |
+
ret = -a
|
550 |
+
}
|
551 |
+
return ret
|
552 |
+
}
|
553 |
+
```
|
554 |
+
|
555 |
+
## Compile-time Constant
|
556 |
+
|
557 |
+
A compile-time constant, CTC for short, is a special variable whose value can be determined at compile time. A CTC must be defined in one of the following ways.
|
558 |
+
|
559 |
+
|
560 |
+
* A number literal like:
|
561 |
+
|
562 |
+
```ts
|
563 |
+
3
|
564 |
+
```
|
565 |
+
|
566 |
+
* A `const` variable, whose value must be a numeric literal. Expressions cannot be used for now.
|
567 |
+
|
568 |
+
```ts
|
569 |
+
const N1 = 3 // valid
|
570 |
+
const N2: number = 3 // invalid, no explicit type `number` allowed
|
571 |
+
const N3 = 3 + 3 // invalid, no expression allowed
|
572 |
+
```
|
573 |
+
|
574 |
+
* A `static` `readonly` property:
|
575 |
+
|
576 |
+
```ts
|
577 |
+
class X {
|
578 |
+
static readonly M1 = 3 // valid
|
579 |
+
static readonly M2: number = 3 // invalid
|
580 |
+
static readonly M3 = 3 + 3 // invalid
|
581 |
+
}
|
582 |
+
```
|
583 |
+
|
584 |
+
|
585 |
+
A CTC is required in these cases.
|
586 |
+
|
587 |
+
* Array size
|
588 |
+
|
589 |
+
```ts
|
590 |
+
let arr1: FixedArray<bigint, 3> = [1n, 2n, 3n]
|
591 |
+
// `typeof` is needed since FixedArray takes a type as the array size, not a value
|
592 |
+
let arr1: FixedArray<bigint, typeof N1> = [1n, 2n, 3n]
|
593 |
+
let arr2: FixedArray<bigint, typeof X.M1> = [1n, 2n, 3n]
|
594 |
+
```
|
595 |
+
|
596 |
+
* Loop count in `for` statement
|
597 |
+
|
598 |
+
```ts
|
599 |
+
for(let i=0; i< 3; i++) {}
|
600 |
+
for(let i=0; i< N1; i++) {}
|
601 |
+
for(let i=0; i< X.M1; i++) {}
|
602 |
+
```
|
603 |
+
|
604 |
+
## Functions
|
605 |
+
|
606 |
+
### Built-in Functions
|
607 |
+
You can refer to [Built-ins](./built-ins.md) for a full list of functions and libraries built into `scryptTS`.
|
608 |
+
|
609 |
+
### Whitelisted Functions
|
610 |
+
Be default, all Javascript/TypeScript built-in functions and global variables are not allowed in `@method`s, except the following kinds.
|
611 |
+
|
612 |
+
#### `console.log`
|
613 |
+
|
614 |
+
`console.log` can be used for debugging purposes.
|
615 |
+
```ts
|
616 |
+
@method()
|
617 |
+
add(x0: bigint, x1:bigint) : bigint {
|
618 |
+
console.log(x0)
|
619 |
+
return x0 + x1
|
620 |
+
}
|
621 |
+
```
|
622 |
+
|
623 |
+
|
624 |
+
## Operators
|
625 |
+
|
626 |
+
**sCrypt** is a subset of TypeScript. Only the following operators can be used directly.
|
627 |
+
|
628 |
+
|
629 |
+
| Operator | Description |
|
630 |
+
| :-----| :----: |
|
631 |
+
| `+` | Addition |
|
632 |
+
| `-` | Subtraction |
|
633 |
+
| `*` | Multiplication |
|
634 |
+
| `/` | Division |
|
635 |
+
| `%` | Remainder |
|
636 |
+
| `++` | Increment |
|
637 |
+
| `--` | Decrement |
|
638 |
+
| `==` | Equal to |
|
639 |
+
| `!=` | Not equal to |
|
640 |
+
| `===` | Same as `==` |
|
641 |
+
| `!==` | Same as `!=` |
|
642 |
+
| `>` | Greater than |
|
643 |
+
| `>=` | Greater than or equal to |
|
644 |
+
| `<` | Less than |
|
645 |
+
| `<=` | Less than or equal to |
|
646 |
+
| `&&` | Logical AND |
|
647 |
+
| <code>||</code> | Logical OR |
|
648 |
+
| `!` | Logical NOT |
|
649 |
+
| `cond ? expr1 : expr2 ` | ternary |
|
650 |
+
| `+=` | Add and assign |
|
651 |
+
| `-=` | Subtract and assign |
|
652 |
+
| `*=` | Multiply and assign |
|
653 |
+
| `/=` | Divide and assign |
|
654 |
+
| `%=` | Assign remainder |
|
655 |
+
|
656 |
+
:::note
|
657 |
+
`**` is not supported currently.
|
658 |
+
:::
|
inline-asm.md
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 6
|
3 |
+
---
|
4 |
+
|
5 |
+
# How To Override Methods Compiled Code
|
6 |
+
|
7 |
+
In some rare cases you might want to use low level Bitcoin script to write a smart contracts method. This is usually done for optimization of the scrip size.
|
8 |
+
To achieve this currently, you have to edit the transpiled `.scrypt` files under your projects artifacts directory.
|
9 |
+
|
10 |
+
To make this a bit easier, you can re-use the code inside the `optimizations/` directory in [`scrypt-ts-lib`](https://github.com/sCrypt-Inc/scrypt-ts-lib/tree/master/optimizations).
|
11 |
+
|
12 |
+
Inside this directory, there is a shell script named `apply_asm_optim.sh`. In this script, you can specify the source files where substitution with a custom bitcoin script should occur.
|
13 |
+
|
14 |
+
Let's take a quick look at how this is applied in `scrypt-ts-lib` for three source files:
|
15 |
+
|
16 |
+
```sh
|
17 |
+
# BN256
|
18 |
+
apply artifacts/src/ec/bn256.scrypt optimizations/ec/bn256
|
19 |
+
|
20 |
+
# SECP256K1
|
21 |
+
apply artifacts/src/ec/secp256k1.scrypt optimizations/ec/secp256k1
|
22 |
+
|
23 |
+
# SECP256R1
|
24 |
+
apply artifacts/src/ec/secp256r1.scrypt optimizations/ec/secp256r1
|
25 |
+
```
|
26 |
+
|
27 |
+
For instance, let's consider the content of `optimizations/ec/bn256`:
|
28 |
+
|
29 |
+
```
|
30 |
+
_addCurvePoints.asm
|
31 |
+
doubleCurvePoint.asm
|
32 |
+
lineFuncAdd.asm
|
33 |
+
lineFuncDouble.asm
|
34 |
+
modInverseBranchlessP.asm
|
35 |
+
mulFQ12.asm
|
36 |
+
mulLine.asm
|
37 |
+
squareFQ12.asm
|
38 |
+
```
|
39 |
+
|
40 |
+
The name of each file corresponds to a function in the specified `.scrypt` source file (`bn256.scrypt` for this example). The files contain the actual bitcoin script in ASM format:
|
41 |
+
|
42 |
+
```
|
43 |
+
OP_3 OP_PICK 11 OP_PICK OP_MUL 12 OP_PICK OP_4 OP_PICK OP_MUL OP_ADD OP_3 OP_PICK 12 OP_PICK OP_MUL OP_5 OP_PICK 14 OP_PICK OP_MUL OP_SUB OP_7 OP_PICK 11 OP_PICK OP_MUL 12 OP_PICK OP_8 OP_PICK OP_MUL OP_ADD OP_7 OP_PICK 12 OP_PICK OP_MUL OP_9 OP_PICK 14 OP_PICK OP_MUL OP_SUB OP_3 OP_ROLL OP_ROT OP_ADD OP_ROT OP_ROT OP_ADD OP_4 OP_PICK 11 OP_PICK 13 OP_PICK OP_8 OP_PICK OP_DUP OP_3 OP_PICK OP_MUL OP_2 OP_PICK OP_5 OP_PICK OP_MUL OP_ADD OP_SWAP OP_ROT OP_MUL OP_3 OP_ROLL OP_3 ...
|
44 |
+
```
|
45 |
+
|
46 |
+
Please note that it is crucial to run `apply_asm_optim.sh` after each project build. To make this process more convenient, you can modify the build script in `package.json`:
|
47 |
+
|
48 |
+
```json
|
49 |
+
"scripts": {
|
50 |
+
"build": "tsc && npm run apply-optim",
|
51 |
+
"apply-optim": "sh optimizations/apply_asm_optim.sh",
|
52 |
+
...
|
53 |
+
```
|
54 |
+
|
55 |
+
Now, after every build, the script optimizations will be applied.
|
56 |
+
|
57 |
+
:::note
|
58 |
+
Please bear in mind that modifying the contract's script code may cause inconsistencies between the on-chain and local execution (methods TS code) behavior. Once you modify the Bitcoin script, it is your responsibility to keep the two versions functionally equivalent.
|
59 |
+
:::
|
installation.md
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 2
|
3 |
+
---
|
4 |
+
|
5 |
+
# Installation
|
6 |
+
|
7 |
+
## Prerequisite
|
8 |
+
|
9 |
+
1. Install `Node.js` and `NPM` on your machine by following the instructions over [here](https://nodejs.org/en/download).
|
10 |
+
|
11 |
+
:::note
|
12 |
+
Require `Node.js` version `>=16`.
|
13 |
+
:::
|
14 |
+
|
15 |
+
2. Install [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
|
16 |
+
|
17 |
+
## The sCrypt CLI Tool
|
18 |
+
|
19 |
+
The [sCrypt CLI tool](https://github.com/sCrypt-Inc/scrypt-cli) is used to easily create, compile and publish `sCrypt` projects. The CLI provides best practice project scaffolding including dependencies such as sCrypt, a test framework ([Mocha](https://mochajs.org)), code auto-formatting ([Prettier](https://prettier.io)), linting ([ES Lint](https://eslint.org)), & more.
|
20 |
+
|
21 |
+
Install it and try it out by creating a demo project:
|
22 |
+
```sh
|
23 |
+
npm install -g scrypt-cli
|
24 |
+
scrypt project demo
|
25 |
+
```
|
26 |
+
|
27 |
+
:::tip
|
28 |
+
You can also simply fork [the demo contract Repl](https://replit.com/@msinkec/scryptTS-demo) and play with the code in your browser.
|
29 |
+
:::
|
oracle.md
ADDED
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 3
|
3 |
+
---
|
4 |
+
|
5 |
+
# Tutorial 3: Oracle
|
6 |
+
|
7 |
+
## Overview
|
8 |
+
|
9 |
+
In this tutorial, we will go over how to build a smart contract that consumes off-chain data from an oracle. Specifically, we will implement a smart contract that lets two players bet on the price of BSV at some point in the future. It retrieves prices from an oracle.
|
10 |
+
|
11 |
+
### What is an Oracle?
|
12 |
+
A blockchain oracle is a third-party service or agent that provides external data to a blockchain network. It is a bridge between the blockchain and the external world, enabling smart contracts to access, verify, and incorporate data from outside the blockchain. This allows smart contracts to execute based on real-world events and conditions, enhancing their utility and functionality.
|
13 |
+
|
14 |
+

|
15 |
+
|
16 |
+
[Credit: bitnovo](https://blog.bitnovo.com/en/what-is-a-blockchain-oracle/)
|
17 |
+
|
18 |
+
The data supplied by oracles can include various types of information, such as stock prices, weather data, election results, and sports scores.
|
19 |
+
|
20 |
+
### Rabin Signatures
|
21 |
+
A digital signature is required to verify the authenticity and integrity of arbitrary data provided by known oracles in a smart contract. Instead of ECDSA used in Bitcoin, we use an alternative digital signature algorithm called [Rabin signatures](https://en.wikipedia.org/wiki/Rabin_signature_algorithm). This is because Rabin signature verification is orders of magnitude cheaper than ECDSA.
|
22 |
+
We have implemented [Rabin signature](https://github.com/sCrypt-Inc/scrypt-ts-lib/blob/master/src/rabinSignature.ts) as part of the standard libraries [`scrypt-ts-lib`](https://www.npmjs.com/package/scrypt-ts-lib), which can be imported and used directly.
|
23 |
+
|
24 |
+
## Contract Properties
|
25 |
+
|
26 |
+
Our contract will take signed pricing data from the [WitnessOnChain oracle](https://witnessonchain.com). Depending if the price target is reached or not, it will pay out a reward to one of the two players.
|
27 |
+
|
28 |
+
There are quite a few properties which our price betting smart contract will require:
|
29 |
+
|
30 |
+
```ts
|
31 |
+
// Price target that needs to be reached.
|
32 |
+
@prop()
|
33 |
+
targetPrice: bigint
|
34 |
+
|
35 |
+
// Symbol of the pair, e.g. "BSV_USDC"
|
36 |
+
@prop()
|
37 |
+
symbol: ByteString
|
38 |
+
|
39 |
+
// Timestamp window in which the price target needs to be reached.
|
40 |
+
@prop()
|
41 |
+
timestampFrom: bigint
|
42 |
+
@prop()
|
43 |
+
timestampTo: bigint
|
44 |
+
|
45 |
+
// Oracles Rabin public key.
|
46 |
+
@prop()
|
47 |
+
oraclePubKey: RabinPubKey
|
48 |
+
|
49 |
+
// Addresses of both players.
|
50 |
+
@prop()
|
51 |
+
alicePkh: PubKeyHash
|
52 |
+
@prop()
|
53 |
+
bobPkh: PubKeyHash
|
54 |
+
```
|
55 |
+
|
56 |
+
Notice that the type `RabinPubKey`, which represents a Rabin public key, is not a standard type. You can import it the following way:
|
57 |
+
|
58 |
+
```ts
|
59 |
+
import { RabinPubKey } from 'scrypt-ts-lib'
|
60 |
+
```
|
61 |
+
|
62 |
+
## Public Method - `unlock`
|
63 |
+
|
64 |
+
The contract will have only a single public method, namely `unlock`. As parameters, it will take the oracles signature, the signed message from the oracle, and a signature of the winner, who can unlock the funds:
|
65 |
+
|
66 |
+
```ts
|
67 |
+
@method()
|
68 |
+
public unlock(msg: ByteString, sig: RabinSig, winnerSig: Sig) {
|
69 |
+
// Verify oracle signature.
|
70 |
+
assert(
|
71 |
+
RabinVerifierWOC.verifySig(msg, sig, this.oraclePubKey),
|
72 |
+
'Oracle sig verify failed.'
|
73 |
+
)
|
74 |
+
|
75 |
+
// Decode data.
|
76 |
+
const exchangeRate = PriceBet.parseExchangeRate(msg)
|
77 |
+
|
78 |
+
// Validate data.
|
79 |
+
assert(
|
80 |
+
exchangeRate.timestamp >= this.timestampFrom,
|
81 |
+
'Timestamp too early.'
|
82 |
+
)
|
83 |
+
assert(
|
84 |
+
exchangeRate.timestamp <= this.timestampTo,
|
85 |
+
'Timestamp too late.'
|
86 |
+
)
|
87 |
+
assert(exchangeRate.symbol == this.symbol, 'Wrong symbol.')
|
88 |
+
|
89 |
+
// Decide winner and check their signature.
|
90 |
+
const winner =
|
91 |
+
exchangeRate.price >= this.targetPrice
|
92 |
+
? this.alicePubKey
|
93 |
+
: this.bobPubKey
|
94 |
+
assert(this.checkSig(winnerSig, winner))
|
95 |
+
}
|
96 |
+
```
|
97 |
+
|
98 |
+
Let's walk through each part.
|
99 |
+
|
100 |
+
First, we verify that the passed signature is correct. For that we use the `RabinVerifierWOC` library from the [`scrypt-ts-lib`](https://www.npmjs.com/package/scrypt-ts-lib) package
|
101 |
+
|
102 |
+
```ts
|
103 |
+
import { RabinPubKey, RabinSig, RabinVerifierWoc } from 'scrypt-ts-lib'
|
104 |
+
```
|
105 |
+
|
106 |
+
Now, we can call the `verifySig` method of the verification library:
|
107 |
+
```ts
|
108 |
+
// Verify oracle signature.
|
109 |
+
assert(
|
110 |
+
RabinVerifierWOC.verifySig(msg, sig, this.oraclePubKey),
|
111 |
+
'Oracle sig verify failed.'
|
112 |
+
)
|
113 |
+
```
|
114 |
+
The verification method requires the message signed by the oracle, the oracles signature for the message, and the oracle's public key, which we already set via the constructor.
|
115 |
+
|
116 |
+
Next, we need to parse information from the chunk of data that is the signed message and assert on it. For a granular description of the message format check out the `"Exchange Rate"` section in the [WitnessOnChain API docs](https://witnessonchain.com).
|
117 |
+
|
118 |
+
We need to implement the static method `parseExchangeRate` as follows:
|
119 |
+
|
120 |
+
```ts
|
121 |
+
// Parses signed message from the oracle.
|
122 |
+
@method()
|
123 |
+
static parseExchangeRate(msg: ByteString): ExchangeRate {
|
124 |
+
// 4 bytes timestamp (LE) + 8 bytes rate (LE) + 1 byte decimal + 16 bytes symbol
|
125 |
+
return {
|
126 |
+
timestamp: Utils.fromLEUnsigned(slice(msg, 0n, 4n)),
|
127 |
+
price: Utils.fromLEUnsigned(slice(msg, 4n, 12n)),
|
128 |
+
symbol: slice(msg, 13n, 29n),
|
129 |
+
}
|
130 |
+
}
|
131 |
+
```
|
132 |
+
|
133 |
+
We parse out the following data:
|
134 |
+
- `timestamp` - The time at which this exchange rate is present.
|
135 |
+
- `price` - The exchange rate encoded as an integer -> (priceFloat * (10^decimal)).
|
136 |
+
- `symbol` - The symbol of the token pair, e.g. `BSV_USDC`.
|
137 |
+
|
138 |
+
Finally, we wrap the parsed values in a custom type, named `ExchangeRate` and return it. Here's the definition of the type:
|
139 |
+
|
140 |
+
```ts
|
141 |
+
type ExchangeRate = {
|
142 |
+
timestamp: bigint
|
143 |
+
price: bigint
|
144 |
+
symbol: ByteString
|
145 |
+
}
|
146 |
+
```
|
147 |
+
|
148 |
+
Now we can validate the data. First, we check if the timestamp of the exchange rate is within our specified range that we bet on:
|
149 |
+
|
150 |
+
```ts
|
151 |
+
assert(
|
152 |
+
exchangeRate.timestamp >= this.timestampFrom,
|
153 |
+
'Timestamp too early.'
|
154 |
+
)
|
155 |
+
assert(
|
156 |
+
exchangeRate.timestamp <= this.timestampTo,
|
157 |
+
'Timestamp too late.'
|
158 |
+
)
|
159 |
+
```
|
160 |
+
|
161 |
+
Additionally, we check if the exchange rate is actually for the correct token pair:
|
162 |
+
|
163 |
+
```ts
|
164 |
+
assert(exchangeRate.symbol == this.symbol, 'Wrong symbol.')
|
165 |
+
```
|
166 |
+
|
167 |
+
Lastly, upon having all the necessary information, we can choose the winner and check their signature:
|
168 |
+
|
169 |
+
```ts
|
170 |
+
const winner =
|
171 |
+
exchangeRate.price >= this.targetPrice
|
172 |
+
? this.alicePubKey
|
173 |
+
: this.bobPubKey
|
174 |
+
assert(this.checkSig(winnerSig, winner))
|
175 |
+
```
|
176 |
+
|
177 |
+
As we can see, if the target price is reached, only Alice is able to unlock the funds, and if not, then only Bob is able to do so.
|
178 |
+
|
179 |
+
|
180 |
+
## Conclusion
|
181 |
+
|
182 |
+
Congratulations! You have completed the oracle tutorial!
|
183 |
+
|
184 |
+
The full code along with [tests](https://github.com/sCrypt-Inc/boilerplate/blob/master/tests/local/priceBet.test.ts) can be found in sCrypt's [boilerplate repository](https://github.com/sCrypt-Inc/boilerplate/blob/master/src/contracts/priceBet.ts).
|
185 |
+
|
overview.md
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
slug: /
|
3 |
+
sidebar_position: 1
|
4 |
+
---
|
5 |
+
|
6 |
+
# Overview
|
7 |
+
|
8 |
+
`sCrypt` is an `embedded Domain Specific Language` ([eDSL](https://en.wikipedia.org/wiki/Domain-specific_language#External_and_Embedded_Domain_Specific_Languages)) based on TypeScript for writing smart contracts on Bitcoin SV. `Embedded` means that it is a language inside another language. `sCrypt` is strictly a subset of TypeScript, so all `sCrypt` code is valid TypeScript, but not vice versa.
|
9 |
+
|
10 |
+
We choose [TypeScript](https://www.typescriptlang.org) as the host language because it provides an easy, familiar language (JavaScript), but with type safety, making it easy to get started writing safe smart contracts. There is no need to learn a new programming language or tools, if you are already familiar with TypeScript/JavaScript.
|
11 |
+
If you're new to TypeScript, check out this helpful [introductory video](https://www.youtube.com/watch?v=ahCwqrYpIuM).
|
12 |
+
|
13 |
+
|
14 |
+
## How do Bitcoin Smart Contracts work?
|
15 |
+
|
16 |
+
Smart contracts on Bitcoin are based on the UTXO model, which is very different from an account model like Ethereum used.
|
17 |
+
|
18 |
+
### UTXO model
|
19 |
+
|
20 |
+
Each bitcoin transaction consists of some inputs and outputs.
|
21 |
+
An output contains:
|
22 |
+
|
23 |
+
- The amount of bitcoins it contains.
|
24 |
+
- bytecodes (called the `locking script`).
|
25 |
+
|
26 |
+
while an input contains:
|
27 |
+
- A reference to the previous transaction output.
|
28 |
+
- bytecodes (the `unlocking script`).
|
29 |
+
|
30 |
+
An Unspent Transaction Output (UTXO) is an output not consumed in any transaction yet. The low-level bytecode/opcode is called [Bitcoin Script](https://wiki.bitcoinsv.io/index.php/Script), which is interpreted by the [Bitcoin Virtual Machine](https://xiaohuiliu.medium.com/introduction-to-bitcoin-smart-contracts-9c0ea37dc757) (BVM).
|
31 |
+
|
32 |
+

|
33 |
+
|
34 |
+
In the example above, we have two transactions, each having one input (in green) and one output (in red). And the transaction on the right spends the one on the left.
|
35 |
+
The locking script can be regarded as a boolean function `f` that specifies conditions to spend the bitcoins in the UTXO, acting as a lock (thus the name "locking").
|
36 |
+
The unlocking script in turns provides the function arguments that makes `f` evaluates to `true`, i.e., the "key" (also called witness) needed to unlock.
|
37 |
+
Only when the βkeyβ in an input matches previous outputβs βlockβ, it can spend bitcoins contained in the output.
|
38 |
+
|
39 |
+
In a regular Bitcoin payment to a [Bitcoin address](https://wiki.bitcoinsv.io/index.php/Bitcoin_address), the locking script is [Pay To Pubkey Hash (P2PKH)](https://learnmeabitcoin.com/technical/p2pkh). It checks the spender has the right private key corresponding to the address so she can produce a valid signature in the unlocking script. The expressive Script enables the locking script to specify arbitrarily more complex spending conditions than simple P2PKH, i.e., Bitcoin smart contracts.
|
40 |
+
|
41 |
+
## How does `sCrypt` work?
|
42 |
+
|
43 |
+
`sCrypt` is a high-level language to be compiled into [Bitcoin Script](https://wiki.bitcoinsv.io/index.php/Script). The resulting assembly-like scripts could be used as locking scripts when building transactions.
|
44 |
+
|
45 |
+
## Learn `sCrypt`
|
46 |
+
|
47 |
+
Jump over to the [Installation](./installation.md) section to learn how to create an `sCrypt` project.
|
48 |
+
|
49 |
+
:::tip
|
50 |
+
You can also follow this [Youtube series](https://www.youtube.com/playlist?list=PL0Kn1t30VSpGcbwN-bcbU1-x0fRAoq-GI).
|
51 |
+
:::
|
52 |
+
|
53 |
+
Join the `#scrypt` channel on [Bitcoin SV Discord](https://discord.gg/bsv) or [Slack](https://join.slack.com/t/scryptworkspace/shared_invite/enQtNzQ1OTMyNDk1ODU3LTJmYjE5MGNmNDZhYmYxZWM4ZGY2MTczM2NiNTIxYmFhNTVjNjE5MGYwY2UwNDYxMTQyNGU2NmFkNTY5MmI1MWM) to ask questions.
|
scriptcontext.md
ADDED
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 2
|
3 |
+
---
|
4 |
+
|
5 |
+
# ScriptContext
|
6 |
+
|
7 |
+
In the UTXO model, the context of validating a smart contract is the UTXO containing it and the transaction spending it, including its inputs and outputs. In the following example, when the second of input of transaction `tx1` (2 inputs and 2 outputs) is spending the second output of `tx0` (3 inputs and 3 outputs), the context for the smart contract in the latter output is roughly the UTXO and `tx1` circled in red.
|
8 |
+

|
9 |
+
|
10 |
+
The context only contains local information, different from account-based blockchains whose context consists of the global state of the entire blockchain (as in Ethereum). A single shared global state across all smart contracts kills scalability, since they all have to sequentially processed due to potential race conditions.
|
11 |
+
|
12 |
+
This context is expressed in the `ScriptContext` interface.
|
13 |
+
```ts
|
14 |
+
export interface ScriptContext {
|
15 |
+
/** version number of [transaction]{@link https://wiki.bitcoinsv.io/index.php/Bitcoin_Transactions#General_format_of_a_Bitcoin_transaction} */
|
16 |
+
version: ByteString,
|
17 |
+
/** the specific UTXO spent by this transaction input */
|
18 |
+
utxo: UTXO,
|
19 |
+
/** double-SHA256 hash of the serialization of some/all input outpoints, see [hashPrevouts]{@link https://github.com/bitcoin-sv/bitcoin-sv/blob/master/doc/abc/replay-protected-sighash.md#hashprevouts} */
|
20 |
+
hashPrevouts: ByteString,
|
21 |
+
/** double-SHA256 hash of the serialization of some/all input sequence values, see [hashSequence]{@link https://github.com/bitcoin-sv/bitcoin-sv/blob/master/doc/abc/replay-protected-sighash.md#hashsequence} */
|
22 |
+
hashSequence: ByteString,
|
23 |
+
/** sequence number of [transaction input]{@link https://wiki.bitcoinsv.io/index.php/Bitcoin_Transactions#Format_of_a_Transaction_Input} */
|
24 |
+
sequence: bigint,
|
25 |
+
/** double-SHA256 hash of the serialization of some/all output amount with its locking script, see [hashOutputs]{@link https://github.com/bitcoin-sv/bitcoin-sv/blob/master/doc/abc/replay-protected-sighash.md#hashoutputs} */
|
26 |
+
hashOutputs: ByteString,
|
27 |
+
/** locktime of [transaction]{@link https://wiki.bitcoinsv.io/index.php/Bitcoin_Transactions#General_format_of_a_Bitcoin_transaction} */
|
28 |
+
locktime: bigint,
|
29 |
+
/** [SIGHASH flag]{@link https://wiki.bitcoinsv.io/index.php/SIGHASH_flags} used by this input */
|
30 |
+
sigHashType: SigHashType,
|
31 |
+
}
|
32 |
+
|
33 |
+
export interface UTXO {
|
34 |
+
/** locking script */
|
35 |
+
script: ByteString,
|
36 |
+
/** amount in satoshis */
|
37 |
+
value: bigint,
|
38 |
+
/** outpoint referenced by this UTXO */
|
39 |
+
outpoint: Outpoint,
|
40 |
+
}
|
41 |
+
|
42 |
+
export interface Outpoint {
|
43 |
+
/** txid of the transaction holding the output */
|
44 |
+
txid: ByteString,
|
45 |
+
/** index of the specific output */
|
46 |
+
outputIndex: bigint,
|
47 |
+
}
|
48 |
+
```
|
49 |
+
|
50 |
+
The table shows the meaning of each field of the `ScriptContext` structure.
|
51 |
+
|
52 |
+
| Field | Description |
|
53 |
+
| ------------- | ------------- |
|
54 |
+
| version | version of the transaction |
|
55 |
+
| utxo.value | value of the output spent by this input |
|
56 |
+
| utxo.script | locking script of the UTXO |
|
57 |
+
| utxo.outpoint.txid | txid of the transaction being spent |
|
58 |
+
| utxo.outpoint.outputIndex | index of the UTXO in the outputs |
|
59 |
+
| hashPrevouts | If the `ANYONECANPAY` [SIGHASH type](#sighash-type) is not set, it's double SHA256 of the serialization of all input outpoints . Otherwise, it's a `unit256` of `0x0000......0000`. |
|
60 |
+
| hashSequence | If none of the `ANYONECANPAY`, `SINGLE`, `NONE` [SIGHASH type](#sighash-type) is set, it's double SHA256 of the serialization of sequence of all inputs. Otherwise, it's a `unit256` of `0x0000......0000`. |
|
61 |
+
| sequence | sequence of the input |
|
62 |
+
| hashOutputs | If the [SIGHASH type](#sighash-type) is neither `SINGLE` nor `NONE`, it's double SHA256 of the serialization of all outputs. If the [SIGHASH type](#sighash-type) is `SINGLE` and the input index is smaller than the number of outputs, it's the double SHA256 of the output with the same index as the input. Otherwise, it's a `unit256` of `0x0000......0000`. |
|
63 |
+
| locktime | locktime of the transaction |
|
64 |
+
| sigHashType| sighash type of the signature |
|
65 |
+
|
66 |
+
You can directly access the context through `this.ctx` in any public `@method`.
|
67 |
+
It can be considered additional information a public method gets when called, besides its function parameters.
|
68 |
+
The example below accesses the [locktime](https://learnmeabitcoin.com/technical/locktime) of the spending transaction.
|
69 |
+
|
70 |
+
```ts
|
71 |
+
class CheckLockTimeVerify extends SmartContract {
|
72 |
+
@prop()
|
73 |
+
readonly matureTime: bigint // Can be timestamp or block height.
|
74 |
+
|
75 |
+
constructor(matureTime: bigint) {
|
76 |
+
super(...arguments)
|
77 |
+
this.matureTime = matureTime
|
78 |
+
}
|
79 |
+
|
80 |
+
@method()
|
81 |
+
public timelock() {
|
82 |
+
assert(this.ctx.locktime >= this.matureTime, "locktime too low")
|
83 |
+
}
|
84 |
+
}
|
85 |
+
```
|
86 |
+
|
87 |
+
:::note
|
88 |
+
Accessing `this.ctx` in **non-public** methods is not allowed.
|
89 |
+
:::
|
90 |
+
|
91 |
+
```ts
|
92 |
+
@method()
|
93 |
+
propagateState(outputs: ByteString) : boolean {
|
94 |
+
return this.ctx.hashOutputs == hash256(outputs); // invalid
|
95 |
+
}
|
96 |
+
```
|
97 |
+
|
98 |
+
### Access inputs and outputs
|
99 |
+
|
100 |
+
The inputs and outpus of the spending transaction are not directly included in `ScriptContext`, but their hashes/digests. To access them, we can build them first and validate they hash to the expected digest, which ensures they are actually from the spending transaction.
|
101 |
+
The following example ensure both Alice and Bob get 1000 satoshis from the contract.
|
102 |
+
|
103 |
+
```ts
|
104 |
+
class DesignatedReceivers extends SmartContract {
|
105 |
+
@prop()
|
106 |
+
readonly alice: PubKeyHash
|
107 |
+
|
108 |
+
@prop()
|
109 |
+
readonly bob: PubKeyHash
|
110 |
+
|
111 |
+
constructor(alice: PubKeyHash, bob: PubKeyHash) {
|
112 |
+
super(...arguments)
|
113 |
+
this.alice = alice
|
114 |
+
this.bob = bob
|
115 |
+
}
|
116 |
+
|
117 |
+
@method()
|
118 |
+
public payout() {
|
119 |
+
const aliceOutput: ByteString = Utils.buildPublicKeyHashOutput(alice, 1000n)
|
120 |
+
const bobOutput: ByteString = Utils.buildPublicKeyHashOutput(bob, 1000n)
|
121 |
+
let outputs = aliceOutput + bobOutput
|
122 |
+
|
123 |
+
// require a change output
|
124 |
+
outputs += this.buildChangeOutput();
|
125 |
+
|
126 |
+
// ensure outputs are actually from the spending transaction as expected
|
127 |
+
assert(this.ctx.hashOutputs == hash256(outputs), 'hashOutputs mismatch')
|
128 |
+
}
|
129 |
+
}
|
130 |
+
```
|
131 |
+
|
132 |
+
### SigHash Type
|
133 |
+
|
134 |
+
[SigHash type](https://wiki.bitcoinsv.io/index.php/SIGHASH_flags) decides which part of the spending transaction is included in `ScriptContext`.
|
135 |
+

|
136 |
+
It defaults to `SigHash.ALL`, including all inputs and outputs. You can customize it by setting the argument of the `@method()` decorator, e.g.,
|
137 |
+
|
138 |
+
```ts
|
139 |
+
@method(SigHash.ANYONECANPAY_SINGLE)
|
140 |
+
public increment() {
|
141 |
+
...
|
142 |
+
}
|
143 |
+
```
|
144 |
+
|
145 |
+
There are a total of 6 sigHash types to choose from:
|
146 |
+
|
147 |
+
| SigHash Type | Functional Meaning |
|
148 |
+
| ------------- | ------------- |
|
149 |
+
| ALL | Sign all inputs and outputs |
|
150 |
+
| NONE | Sign all inputs and no output |
|
151 |
+
| SINGLE | Sign all inputs and the output with the same index |
|
152 |
+
| ANYONECANPAY_ALL | Sign its own input and all outputs |
|
153 |
+
| ANYONECANPAY_NONE | Sign its own input and no output |
|
154 |
+
| ANYONECANPAY_SINGLE | Sign its own input and the output with the same index |
|
155 |
+
|
156 |
+
|
157 |
+
|
158 |
+
|
159 |
+
### Debugging
|
160 |
+
|
161 |
+
See [How to Debug ScriptContext Failure](../advanced/how-to-debug-scriptcontext.md)
|
stateful-contract.md
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 3
|
3 |
+
---
|
4 |
+
|
5 |
+
# Stateful Contracts
|
6 |
+
|
7 |
+
## Overview
|
8 |
+
In Bitcoin's UTXO model, a smart contract is one-off and **stateless** by default, since the UTXO containing it is destroyed after being spent. Being stateless allows it to scale easily, the same as in [HTTP](https://stackoverflow.com/questions/5836881/stateless-protocol-and-stateful-protocol) and [REST APIs](https://www.geeksforgeeks.org/restful-statelessness/).
|
9 |
+
A smart contract can simulate state by requiring
|
10 |
+
the output of the spending transaction containing the same contract but with the updated state, enabled by [ScriptContext](scriptcontext.md).
|
11 |
+
This is similar to making HTTP seem stateful by using cookies.
|
12 |
+
|
13 |
+
So far, all the contracts weβve gone through have been stateless. But often, you may want a contract to have some concept of βmemoryβ so that it may remember information about its previous interactions. That is, we need contracts that are **stateful**.
|
14 |
+
|
15 |
+
To achieve that, we divide a smart contract in the locking script of an output into two parts: code and state as shown below. The code part contains the business logic of a contract that encodes rules for state transition and must **NOT** change. State transition occurs when a transaction spends the output containing the old state and creates a new output containing the new state, while keeping the contract code intact.
|
16 |
+
Since the new output contains the same contract code, its spending transaction must also retain the same code, otherwise it will fail. This chain of transactions can go on and on and thus a state is maintained along the chain, recursively.
|
17 |
+

|
18 |
+
|
19 |
+
## Create a Stateful Contract
|
20 |
+
|
21 |
+
We can create a stateful contract using the following command:
|
22 |
+
|
23 |
+
```sh
|
24 |
+
scrypt project --state counter
|
25 |
+
```
|
26 |
+
|
27 |
+
Note the `state` option is turned on.
|
28 |
+
|
29 |
+
This will create a project containing a sample stateful contract named `Counter`. This contract maintains a single state: how many times it has been called since deployment.
|
30 |
+
|
31 |
+
Let's take a look at the contract source file `src/contracts/counter.ts`.
|
32 |
+
|
33 |
+
### Stateful properties
|
34 |
+
As shown [before](how-to-write-a-contract.md#properties), a `@prop(true)` decorator is used to make a property part of the contract state, meaning it can be mutated when the contract gets called.
|
35 |
+
|
36 |
+
```ts
|
37 |
+
@prop(true)
|
38 |
+
count: bigint
|
39 |
+
```
|
40 |
+
|
41 |
+
### Update states
|
42 |
+
|
43 |
+
The `incrementOnChain()` method does two things:
|
44 |
+
|
45 |
+
1. Call `increment` to update the state:
|
46 |
+
|
47 |
+
```ts
|
48 |
+
@method()
|
49 |
+
increment(): void {
|
50 |
+
this.count++
|
51 |
+
}
|
52 |
+
```
|
53 |
+
|
54 |
+
2. Validate the new state goes into the next UTXO containing the same contract, i.e., the state is maintained.
|
55 |
+
|
56 |
+
```ts
|
57 |
+
// make sure balance in the contract does not change
|
58 |
+
const amount: bigint = this.ctx.utxo.value
|
59 |
+
// outputs containing the latest state and an optional change output
|
60 |
+
const outputs: ByteString = this.buildStateOutput(amount) + this.buildChangeOutput()
|
61 |
+
// verify unlocking tx has the same outputs
|
62 |
+
assert(this.ctx.hashOutputs == hash256(outputs), 'hashOutputs mismatch')
|
63 |
+
```
|
64 |
+
|
65 |
+
The built-in function `this.buildStateOutput()` creates an output containing the latest state. It takes an input: the number of satoshis in the output. We keep the satoshis unchanged in the example. The built-in functin `this.buildChangeOutput()` creates a P2PKH change output when necessary. It will calculate the change amount automatically, and use the signer's address by default.
|
66 |
+
|
67 |
+
If all outputs we create in the contract hashes to `hashOutputs` in [ScriptContext](scriptcontext.md), we can be sure they are the outputs of the current transaction. Therefore, the updated state is propagated.
|
68 |
+
|
69 |
+
|
70 |
+
The complete stateful contract is as follows:
|
71 |
+
|
72 |
+
```ts
|
73 |
+
export class Counter extends SmartContract {
|
74 |
+
// stateful
|
75 |
+
@prop(true)
|
76 |
+
count: bigint
|
77 |
+
|
78 |
+
constructor(count: bigint) {
|
79 |
+
super(...arguments)
|
80 |
+
this.count = count
|
81 |
+
}
|
82 |
+
|
83 |
+
@method()
|
84 |
+
public incrementOnChain() {
|
85 |
+
this.increment()
|
86 |
+
|
87 |
+
// make sure balance in the contract does not change
|
88 |
+
const amount: bigint = this.ctx.utxo.value
|
89 |
+
// outputs containing the latest state and an optional change output
|
90 |
+
const outputs: ByteString = this.buildStateOutput(amount) + this.buildChangeOutput()
|
91 |
+
// verify unlocking tx has the same outputs
|
92 |
+
assert(this.ctx.hashOutputs == hash256(outputs), 'hashOutputs mismatch')
|
93 |
+
}
|
94 |
+
|
95 |
+
@method()
|
96 |
+
increment(): void {
|
97 |
+
this.count++
|
98 |
+
}
|
99 |
+
}
|
100 |
+
```
|
101 |
+
|
102 |
+
## Stateless vs Stateful Contracts
|
103 |
+
|
104 |
+
The choice between stateless and stateful smart contracts hinges on the needs of your blockchain application.
|
105 |
+
|
106 |
+
If your app needs to store persistent data on chain, a stateful smart contract is appropriate. For example, with an [auction app](../tutorials/auction.md), you want to store the highest bidder so far and how much she bids, in case you need to return the fund to her when a higher bid arrives.
|
107 |
+
|
108 |
+
If your app merely validates spending conditions without retaining data, a stateless smart contract is desirable. An example is a simple transfer using signature and public key in a [P2PKH contract](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#method-with-signatures).
|
tic-tac-toe.md
ADDED
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 4
|
3 |
+
---
|
4 |
+
|
5 |
+
# Tutorial 4: Tic Tac Toe
|
6 |
+
|
7 |
+
## Overview
|
8 |
+
In this tutorial, we will go over how to use sCrypt to build a Tic-Tac-Toe Contract on Bitcoin.
|
9 |
+
|
10 |
+
It is initialized with the Bitcoin public key of two players (Alice and Bob respectively). They each bet the same amount and lock it into the contract. The winner takes all bitcoins locked in the contract. If no one wins and there is a draw, the two players can each withdraw half of the money.
|
11 |
+
|
12 |
+
## Contract Properties
|
13 |
+
|
14 |
+
Use `@prop` decorator to mark any property that intends to be stored on chain. This decorator accepts a boolean parameter. By default, it is set to `false`, meaning the property cannot be changed after the contract is deployed. If it is `true`, the property is a so-called [stateful](../how-to-write-a-contract/stateful-contract.md) property and its value can be updated in subsequent contract calls.
|
15 |
+
|
16 |
+
The tic-tac-toe contract supports two players and their public keys need to be saved. It contains the following contract properties:
|
17 |
+
|
18 |
+
- Two stateless properties `alice` and `bob`, both of which are `PubKey` type.
|
19 |
+
- Two stateful properties:
|
20 |
+
* `is_alice_turn`: a `boolean`. It represents whether it is `alice`'s turn to play.
|
21 |
+
* `board`: a fixed-size array `FixedArray<bigint, 9>` with a size of `9`. It represents the state of every square in the board.
|
22 |
+
- Three constants:
|
23 |
+
* `EMPTY`, type `bigint`, value `0n`. It means that a square in the board is empty
|
24 |
+
* `ALICE`, type `bigint`, value `1n`. Alice places symbol `X` in a square.
|
25 |
+
* `BOB`, type `bigint`, value `2n`. Bob places symbol `O` in a square.
|
26 |
+
|
27 |
+
```ts
|
28 |
+
@prop()
|
29 |
+
alice: PubKey; // alice's public Key
|
30 |
+
@prop()
|
31 |
+
bob: PubKey; // bob's public Key
|
32 |
+
|
33 |
+
@prop(true)
|
34 |
+
is_alice_turn: boolean; // stateful property, it represents whether it is `alice`'s turn to play.
|
35 |
+
|
36 |
+
@prop(true)
|
37 |
+
board: FixedArray<bigint, 9>; // stateful property, a fixed-size array, it represents the state of every square in the board.
|
38 |
+
|
39 |
+
@prop()
|
40 |
+
static readonly EMPTY: bigint = 0n; // static property, it means that the a square in the board is empty
|
41 |
+
@prop()
|
42 |
+
static readonly ALICE: bigint = 1n; // static property, it means that alice places symbol `X` in a square.
|
43 |
+
@prop()
|
44 |
+
static readonly BOB: bigint = 2n; // static property, it means that bob places symbol `O` in a square.
|
45 |
+
```
|
46 |
+
|
47 |
+
## Constructor
|
48 |
+
|
49 |
+
Initialize all non-static properties in the constructor. Specifically, the entire board is empty at first.
|
50 |
+
|
51 |
+
```ts
|
52 |
+
constructor(alice: PubKey, bob: PubKey) {
|
53 |
+
super(...arguments);
|
54 |
+
this.alice = alice;
|
55 |
+
this.bob = bob;
|
56 |
+
this.is_alice_turn = true;
|
57 |
+
this.board = fill(TicTacToe.EMPTY, 9);
|
58 |
+
}
|
59 |
+
```
|
60 |
+
|
61 |
+
## Public Methods
|
62 |
+
|
63 |
+
A public `@method` can be called from an external transaction. The call succeeds if it runs to completion without violating any conditions in `assert()`.
|
64 |
+
|
65 |
+
The `TicTacToe` contract have a public `@method` called `move()` with `2` parameters:
|
66 |
+
|
67 |
+
```ts
|
68 |
+
/**
|
69 |
+
* play the game by calling move()
|
70 |
+
* @param n which square to place the symbol
|
71 |
+
* @param sig a player's signature
|
72 |
+
*/
|
73 |
+
@method()
|
74 |
+
public move(n: bigint, sig: Sig) {
|
75 |
+
assert(n >= 0n && n < 9n);
|
76 |
+
}
|
77 |
+
```
|
78 |
+
|
79 |
+
Alice and Bob each locks X bitcoins in a UTXO containing contract `TicTacToe`. Next, they alternately play the game by calling `move()`.
|
80 |
+
|
81 |
+
### Signature Verification
|
82 |
+
|
83 |
+
Once the game contract is deployed, anyone can view and potentially interact with it. We need a authentication mechanism to ensure only the desired player can update the contract if it's their turn. This is achieved using ditigal signatures.
|
84 |
+
|
85 |
+
`this.checkSig()` is used to verify a signature against a public key. Use it to verify the `sig` parameter against the desired player in `move()`, identified by their public key stored in the contract's properties.
|
86 |
+
|
87 |
+
```ts
|
88 |
+
// check signature `sig`
|
89 |
+
let player: PubKey = this.is_alice_turn ? this.alice : this.bob;
|
90 |
+
assert(this.checkSig(sig, player), `checkSig failed, pubkey: ${player}`);
|
91 |
+
```
|
92 |
+
|
93 |
+
## Non-Public Methods
|
94 |
+
|
95 |
+
Without a `public` modifier, a `@method` is internal and cannot be directly called from an external transaction.
|
96 |
+
|
97 |
+
The `TicTacToe` contract have two **Non-Public** methods:
|
98 |
+
|
99 |
+
- `won()` : iterate over the `lines` array to check if a player has won the game. returns `boolean` type.
|
100 |
+
- `full()` : traverse all the squares of the board to check if all squares of the board have symbols. returns `boolean` type.
|
101 |
+
|
102 |
+
|
103 |
+
```ts
|
104 |
+
@method()
|
105 |
+
won(play: bigint) : boolean {
|
106 |
+
let lines: FixedArray<FixedArray<bigint, 3>, 8> = [
|
107 |
+
[0n, 1n, 2n],
|
108 |
+
[3n, 4n, 5n],
|
109 |
+
[6n, 7n, 8n],
|
110 |
+
[0n, 3n, 6n],
|
111 |
+
[1n, 4n, 7n],
|
112 |
+
[2n, 5n, 8n],
|
113 |
+
[0n, 4n, 8n],
|
114 |
+
[2n, 4n, 6n]
|
115 |
+
];
|
116 |
+
|
117 |
+
let anyLine = false;
|
118 |
+
|
119 |
+
for (let i = 0; i < 8; i++) {
|
120 |
+
let line = true;
|
121 |
+
for (let j = 0; j < 3; j++) {
|
122 |
+
line = line && this.board[Number(lines[i][j])] === play;
|
123 |
+
}
|
124 |
+
|
125 |
+
anyLine = anyLine || line;
|
126 |
+
}
|
127 |
+
|
128 |
+
return anyLine;
|
129 |
+
}
|
130 |
+
|
131 |
+
@method()
|
132 |
+
full() : boolean {
|
133 |
+
let full = true;
|
134 |
+
for (let i = 0; i < 9; i++) {
|
135 |
+
full = full && this.board[i] !== TicTacToe.EMPTY;
|
136 |
+
}
|
137 |
+
return full;
|
138 |
+
}
|
139 |
+
```
|
140 |
+
|
141 |
+
## Maintain Game State
|
142 |
+
|
143 |
+
We can directly access the [ScriptContext](../how-to-write-a-contract/scriptcontext.md) through `this.ctx` in the public `@method` `move()` to maintain game state. It can be considered additional information a public method gets when called, besides its function parameters.
|
144 |
+
|
145 |
+
Contract maintenance state consists of the following three steps:
|
146 |
+
|
147 |
+
### Step 1
|
148 |
+
|
149 |
+
Update the stateful properties in public `@method`.
|
150 |
+
|
151 |
+
A player call `move()` to places the symbol in the board. We should update the stateful properties `board` and `is_alice_turn` in the `move()` `@method`:
|
152 |
+
|
153 |
+
```ts
|
154 |
+
assert(this.board[Number(n)] === TicTacToe.EMPTY, `board at position ${n} is not empty: ${this.board[Number(n)]}`);
|
155 |
+
let play = this.is_alice_turn ? TicTacToe.ALICE : TicTacToe.BOB;
|
156 |
+
// update stateful properties to make the move
|
157 |
+
this.board[Number(n)] = play; // Number() converts a bigint to a number
|
158 |
+
this.is_alice_turn = !this.is_alice_turn;
|
159 |
+
```
|
160 |
+
|
161 |
+
### Step 2
|
162 |
+
|
163 |
+
When you are ready to pass the new state onto the output[s] in the current spending transaction, simply call a built-in function `this.buildStateOutput()` to create an output containing the new state. It takes an input: the number of satoshis in the output. We keep the satoshis unchanged in the example.
|
164 |
+
|
165 |
+
```ts
|
166 |
+
let output = this.buildStateOutput(this.ctx.utxo.value);
|
167 |
+
```
|
168 |
+
|
169 |
+
|
170 |
+
|
171 |
+
#### Build outputs in public `@method`
|
172 |
+
|
173 |
+
`TicTacToe` can contain the following three types of output during execution:
|
174 |
+
|
175 |
+
1. The game is not over: a output containing the new state and a change output
|
176 |
+
2. A player wins the game: a `P2PKH` output that pays the winner, and a change output.
|
177 |
+
3. A draw: two `P2PKH` outputs that split the contract-locked bets equally between the players and a change output.
|
178 |
+
|
179 |
+
The `P2PKH` output can be built using `Utils.buildPublicKeyHashOutput(pkh: PubKeyHash, amount: bigint)`. The [change output](https://wiki.bitcoinsv.io/index.php/Change) can be built using `this.buildChangeOutput()`.
|
180 |
+
|
181 |
+
|
182 |
+
```ts
|
183 |
+
// build the transation outputs
|
184 |
+
let outputs = toByteString('');
|
185 |
+
if (this.won(play)) {
|
186 |
+
outputs = Utils.buildPublicKeyHashOutput(hash160(player), this.ctx.utxo.value);
|
187 |
+
}
|
188 |
+
else if (this.full()) {
|
189 |
+
const halfAmount = this.ctx.utxo.value / 2n;
|
190 |
+
const aliceOutput = Utils.buildPublicKeyHashOutput(hash160(this.alice), halfAmount);
|
191 |
+
const bobOutput = Utils.buildPublicKeyHashOutput(hash160(this.bob), halfAmount);
|
192 |
+
outputs = aliceOutput + bobOutput;
|
193 |
+
}
|
194 |
+
else {
|
195 |
+
// build a output that contains latest contract state.
|
196 |
+
outputs = this.buildStateOutput(this.ctx.utxo.value);
|
197 |
+
}
|
198 |
+
|
199 |
+
outputs += this.buildChangeOutput();
|
200 |
+
|
201 |
+
```
|
202 |
+
|
203 |
+
### Step 3
|
204 |
+
|
205 |
+
Make sure that the output of the current transaction must contain this incremented new state. If all outputs (only a single output here) we create in the contract hashes to `hashOutputs` in `ScriptContext`, we can be sure they are the outputs of the current transaction. Therefore, the updated state is propagated.
|
206 |
+
|
207 |
+
```ts
|
208 |
+
// verify current tx has this single output
|
209 |
+
assert(this.ctx.hashOutputs == hash256(outputs), 'hashOutputs mismatch')
|
210 |
+
```
|
211 |
+
|
212 |
+
# Conclusion
|
213 |
+
|
214 |
+
Congratulations, you have completed the `TicTacToe` contract!
|
215 |
+
|
216 |
+
The [final complete code](https://github.com/sCrypt-Inc/tic-tac-toe/blob/main/src/contracts/tictactoe.ts) is as follows:
|
217 |
+
|
218 |
+
```ts
|
219 |
+
export class TicTacToe extends SmartContract {
|
220 |
+
@prop()
|
221 |
+
alice: PubKey;
|
222 |
+
@prop()
|
223 |
+
bob: PubKey;
|
224 |
+
|
225 |
+
@prop(true)
|
226 |
+
is_alice_turn: boolean;
|
227 |
+
|
228 |
+
@prop(true)
|
229 |
+
board: FixedArray<bigint, 9>;
|
230 |
+
|
231 |
+
@prop()
|
232 |
+
static readonly EMPTY: bigint = 0n;
|
233 |
+
@prop()
|
234 |
+
static readonly ALICE: bigint = 1n;
|
235 |
+
@prop()
|
236 |
+
static readonly BOB: bigint = 2n;
|
237 |
+
|
238 |
+
constructor(alice: PubKey, bob: PubKey) {
|
239 |
+
super(...arguments)
|
240 |
+
this.alice = alice;
|
241 |
+
this.bob = bob;
|
242 |
+
this.is_alice_turn = true;
|
243 |
+
this.board = fill(TicTacToe.EMPTY, 9);
|
244 |
+
}
|
245 |
+
|
246 |
+
@method()
|
247 |
+
public move(n: bigint, sig: Sig) {
|
248 |
+
// check position `n`
|
249 |
+
assert(n >= 0n && n < 9n);
|
250 |
+
// check signature `sig`
|
251 |
+
let player: PubKey = this.is_alice_turn ? this.alice : this.bob;
|
252 |
+
assert(this.checkSig(sig, player), `checkSig failed, pubkey: ${player}`);
|
253 |
+
// update stateful properties to make the move
|
254 |
+
assert(this.board[Number(n)] === TicTacToe.EMPTY, `board at position ${n} is not empty: ${this.board[Number(n)]}`);
|
255 |
+
let play = this.is_alice_turn ? TicTacToe.ALICE : TicTacToe.BOB;
|
256 |
+
this.board[Number(n)] = play;
|
257 |
+
this.is_alice_turn = !this.is_alice_turn;
|
258 |
+
|
259 |
+
// build the transation outputs
|
260 |
+
let outputs = toByteString('');
|
261 |
+
if (this.won(play)) {
|
262 |
+
outputs = Utils.buildPublicKeyHashOutput(hash160(player), this.ctx.utxo.value);
|
263 |
+
}
|
264 |
+
else if (this.full()) {
|
265 |
+
const halfAmount = this.ctx.utxo.value / 2n;
|
266 |
+
const aliceOutput = Utils.buildPublicKeyHashOutput(hash160(this.alice), halfAmount);
|
267 |
+
const bobOutput = Utils.buildPublicKeyHashOutput(hash160(this.bob), halfAmount);
|
268 |
+
outputs = aliceOutput + bobOutput;
|
269 |
+
}
|
270 |
+
else {
|
271 |
+
// build a output that contains latest contract state.
|
272 |
+
outputs = this.buildStateOutput(this.ctx.utxo.value);
|
273 |
+
}
|
274 |
+
|
275 |
+
outputs += this.buildChangeOutput();
|
276 |
+
|
277 |
+
// make sure the transaction contains the expected outputs built above
|
278 |
+
assert(this.ctx.hashOutputs === hash256(outputs), "check hashOutputs failed");
|
279 |
+
}
|
280 |
+
|
281 |
+
@method()
|
282 |
+
won(play: bigint): boolean {
|
283 |
+
let lines: FixedArray<FixedArray<bigint, 3>, 8> = [
|
284 |
+
[0n, 1n, 2n],
|
285 |
+
[3n, 4n, 5n],
|
286 |
+
[6n, 7n, 8n],
|
287 |
+
[0n, 3n, 6n],
|
288 |
+
[1n, 4n, 7n],
|
289 |
+
[2n, 5n, 8n],
|
290 |
+
[0n, 4n, 8n],
|
291 |
+
[2n, 4n, 6n]
|
292 |
+
];
|
293 |
+
|
294 |
+
let anyLine = false;
|
295 |
+
|
296 |
+
for (let i = 0; i < 8; i++) {
|
297 |
+
let line = true;
|
298 |
+
for (let j = 0; j < 3; j++) {
|
299 |
+
line = line && this.board[Number(lines[i][j])] === play;
|
300 |
+
}
|
301 |
+
|
302 |
+
anyLine = anyLine || line;
|
303 |
+
}
|
304 |
+
|
305 |
+
return anyLine;
|
306 |
+
}
|
307 |
+
|
308 |
+
@method()
|
309 |
+
full(): boolean {
|
310 |
+
let full = true;
|
311 |
+
for (let i = 0; i < 9; i++) {
|
312 |
+
full = full && this.board[i] !== TicTacToe.EMPTY;
|
313 |
+
}
|
314 |
+
return full;
|
315 |
+
}
|
316 |
+
|
317 |
+
}
|
318 |
+
|
319 |
+
```
|
320 |
+
|
321 |
+
But no dApp is complete if users cannot interact with it. Go [here](../how-to-integrate-a-frontend/how-to-integrate-a-frontend.md) to see how to add a front end to it.
|
voting.md
ADDED
@@ -0,0 +1,706 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 6
|
3 |
+
---
|
4 |
+
|
5 |
+
# Tutorial 6: Voting
|
6 |
+
|
7 |
+
## Overview
|
8 |
+
|
9 |
+
In this tutorial, we will go over how to use sCrypt to build a full-stack voting dApp on Bitcoin, including the smart contract and an interactive front-end.
|
10 |
+
|
11 |
+

|
12 |
+
|
13 |
+
On the web page, you can see the candidate list. Clicking the like button will cast one vote for the corresponding candidate. This will prompt the wallet to ask for a user's approval. A transaction calling the contract will be sent after her approval.
|
14 |
+
|
15 |
+
First, we will write and deploy the smart contract step by step. Afterward, we will build a front-end with React that allows users to cast votes and thus interact with the contract.
|
16 |
+
|
17 |
+
## Contract
|
18 |
+
|
19 |
+
### Properties
|
20 |
+
|
21 |
+
For each candidate, there are two properties we need to store in the contract: her name and her votes received so far.
|
22 |
+
|
23 |
+
We define a type alias of `ByteString` to represent a candidate name.
|
24 |
+
|
25 |
+
```ts
|
26 |
+
export type Name = ByteString
|
27 |
+
```
|
28 |
+
|
29 |
+
We define a struct to represent a candidate.
|
30 |
+
|
31 |
+
```ts
|
32 |
+
export type Candidate = {
|
33 |
+
name: Name
|
34 |
+
votesReceived: bigint
|
35 |
+
}
|
36 |
+
```
|
37 |
+
|
38 |
+
We use a `FixedArray` to store the list of candidates, which we alias as type `Candidates`.
|
39 |
+
Since candidates' vote counts can be updated, we mark it [stateful](../how-to-write-a-contract/stateful-contract.md#stateful-properties) by setting `@prop(true)`.
|
40 |
+
|
41 |
+
```ts
|
42 |
+
export const N = 2
|
43 |
+
export type Candidates = FixedArray<Candidate, typeof N>
|
44 |
+
|
45 |
+
export class Voting extends SmartContract {
|
46 |
+
@prop(true)
|
47 |
+
candidates: Candidates
|
48 |
+
// ...
|
49 |
+
}
|
50 |
+
```
|
51 |
+
|
52 |
+
### Constructor
|
53 |
+
|
54 |
+
Initialize all the `@prop` properties in the constructor. Note that we only need to pass the candidate names in the argument, because the votes they received would be all 0 at the beginning.
|
55 |
+
|
56 |
+
```ts
|
57 |
+
constructor(names: FixedArray<Name, typeof N>) {
|
58 |
+
super(...arguments)
|
59 |
+
// initialize fixed array
|
60 |
+
this.candidates = fill({
|
61 |
+
name: toByteString(''),
|
62 |
+
votesReceived: 0n
|
63 |
+
}, N)
|
64 |
+
// set names and set votes they received to 0
|
65 |
+
for (let i = 0; i < N; i++) {
|
66 |
+
this.candidates[i] = { name: names[i], votesReceived: 0n }
|
67 |
+
}
|
68 |
+
}
|
69 |
+
```
|
70 |
+
|
71 |
+
### Methods
|
72 |
+
|
73 |
+
The only way to interact with this contract is to vote for one candidate in the list, so we will have only 1 **public** method `vote`. It takes only 1 parameter: the name of the candidate you want to vote for.
|
74 |
+
|
75 |
+
```ts
|
76 |
+
@method()
|
77 |
+
public vote(name: Name) {
|
78 |
+
// 1) change contract state: add one vote to `candidate` in the list
|
79 |
+
// 2) propogate the state
|
80 |
+
}
|
81 |
+
```
|
82 |
+
|
83 |
+
We can simply use a `for` loop to implement this: find the corresponding candidate in the list by name, then increment its vote by one. We implement this in a helper method `increaseVotesReceived`.
|
84 |
+
|
85 |
+
```ts
|
86 |
+
// cast one vote to a candidate
|
87 |
+
@method()
|
88 |
+
increaseVotesReceived(name: Name): void {
|
89 |
+
for (let i = 0; i < N; i++) {
|
90 |
+
if (this.candidates[i].name === name) {
|
91 |
+
this.candidates[i].votesReceived++
|
92 |
+
}
|
93 |
+
}
|
94 |
+
}
|
95 |
+
```
|
96 |
+
|
97 |
+
After we increment the candidate's votes and update the contract state, we make sure the new state is maintained in the spending transaction's output [as usual](../how-to-write-a-contract/stateful-contract.md#update-states). Another output is added if change is needed.
|
98 |
+
|
99 |
+
```ts
|
100 |
+
let outputs: ByteString = this.buildStateOutput(this.ctx.utxo.value)
|
101 |
+
outputs += this.buildChangeOutput()
|
102 |
+
assert(this.ctx.hashOutputs === hash256(outputs), 'hashOutputs mismatch')
|
103 |
+
```
|
104 |
+
|
105 |
+
The public function `vote` is now finished.
|
106 |
+
|
107 |
+
```ts
|
108 |
+
@method()
|
109 |
+
public vote(name: Name) {
|
110 |
+
// change contract state: add one vote to `candidate` in the list
|
111 |
+
this.increaseVotesReceived(name)
|
112 |
+
|
113 |
+
// restrict tx outputs
|
114 |
+
// to contain the latest state with the same balance
|
115 |
+
let outputs: ByteString = this.buildStateOutput(this.ctx.utxo.value)
|
116 |
+
// to contain the change output when necessary
|
117 |
+
outputs += this.buildChangeOutput()
|
118 |
+
|
119 |
+
assert(this.ctx.hashOutputs === hash256(outputs), 'hashOutputs mismatch')
|
120 |
+
}
|
121 |
+
```
|
122 |
+
|
123 |
+
### Final Code
|
124 |
+
|
125 |
+
You have completed the `Voting` contract! The [final complete code](https://github.com/sCrypt-Inc/voting/blob/master/src/contracts/voting.ts) is as follows:
|
126 |
+
|
127 |
+
```ts
|
128 |
+
import { assert, ByteString, hash256, method, prop, SmartContract, FixedArray, fill, toByteString } from 'scrypt-ts'
|
129 |
+
|
130 |
+
export type Name = ByteString
|
131 |
+
|
132 |
+
export type Candidate = {
|
133 |
+
name: Name
|
134 |
+
votesReceived: bigint
|
135 |
+
}
|
136 |
+
|
137 |
+
export const N = 2
|
138 |
+
|
139 |
+
export type Candidates = FixedArray<Candidate, typeof N>
|
140 |
+
|
141 |
+
export class Voting extends SmartContract {
|
142 |
+
@prop(true)
|
143 |
+
candidates: Candidates
|
144 |
+
|
145 |
+
constructor(names: FixedArray<Name, typeof N>) {
|
146 |
+
super(...arguments)
|
147 |
+
// initialize fixed array
|
148 |
+
this.candidates = fill({
|
149 |
+
name: toByteString(''),
|
150 |
+
votesReceived: 0n,
|
151 |
+
}, N)
|
152 |
+
// set names and set votes they received to 0
|
153 |
+
for (let i = 0; i < N; i++) {
|
154 |
+
this.candidates[i] = {
|
155 |
+
name: names[i],
|
156 |
+
votesReceived: 0n,
|
157 |
+
}
|
158 |
+
}
|
159 |
+
}
|
160 |
+
|
161 |
+
/**
|
162 |
+
* vote for a candidate
|
163 |
+
* @param name candidate's name
|
164 |
+
*/
|
165 |
+
@method()
|
166 |
+
public vote(name: Name) {
|
167 |
+
// change contract state: add one vote to `candidate` in the list
|
168 |
+
this.increaseVotesReceived(name)
|
169 |
+
// output containing the latest state and the same balance
|
170 |
+
let outputs: ByteString = this.buildStateOutput(this.ctx.utxo.value)
|
171 |
+
outputs += this.buildChangeOutput()
|
172 |
+
assert(this.ctx.hashOutputs === hash256(outputs), 'hashOutputs mismatch')
|
173 |
+
}
|
174 |
+
|
175 |
+
@method()
|
176 |
+
increaseVotesReceived(name: Name): void {
|
177 |
+
for (let i = 0; i < N; i++) {
|
178 |
+
if (this.candidates[i].name === name) {
|
179 |
+
this.candidates[i].votesReceived++
|
180 |
+
}
|
181 |
+
}
|
182 |
+
}
|
183 |
+
}
|
184 |
+
```
|
185 |
+
|
186 |
+
## Frontend
|
187 |
+
|
188 |
+
We will add a frontend to the voting smart contract according to [this guide](../how-to-integrate-a-frontend/how-to-integrate-a-frontend.md).
|
189 |
+
|
190 |
+
### Setup Project
|
191 |
+
|
192 |
+
The front-end will be created using [Create React App](https://create-react-app.dev/).
|
193 |
+
|
194 |
+
```bash
|
195 |
+
npx create-react-app voting --template typescript
|
196 |
+
```
|
197 |
+
|
198 |
+
### Install the sCrypt SDK
|
199 |
+
|
200 |
+
The sCrypt SDK enables you to easily compile, test, deploy, and call contracts.
|
201 |
+
|
202 |
+
Use the `scrypt-cli` command line to install the SDK.
|
203 |
+
|
204 |
+
```bash
|
205 |
+
cd voting
|
206 |
+
npx scrypt-cli init
|
207 |
+
```
|
208 |
+
|
209 |
+
This command will create a contract file at `src\contracts\voting.ts`, replace the content of the file with the contract written [above](#final-code).
|
210 |
+
|
211 |
+
### Compile Contract
|
212 |
+
|
213 |
+
Compile the contract with the following command:
|
214 |
+
|
215 |
+
```bash
|
216 |
+
npx scrypt-cli compile
|
217 |
+
```
|
218 |
+
|
219 |
+
This command will generate a contract artifact file at `artifacts\src\contracts\voting.json`.
|
220 |
+
|
221 |
+
### Contract Deployment
|
222 |
+
|
223 |
+
After [installing the sCrypt SDK](#install-the-scrypt-sdk), you will have a script `deploy.ts` in the project directory, which can be used to deploy our `Voting` contract after some minor modifications.
|
224 |
+
|
225 |
+
```ts
|
226 |
+
import { Name, Voting, N } from './src/contracts/voting'
|
227 |
+
import { bsv, TestWallet, DefaultProvider, toByteString, FixedArray } from 'scrypt-ts'
|
228 |
+
|
229 |
+
import * as dotenv from 'dotenv'
|
230 |
+
|
231 |
+
// Load the .env file
|
232 |
+
dotenv.config()
|
233 |
+
|
234 |
+
// Read the private key from the .env file.
|
235 |
+
// The default private key inside the .env file is meant to be used for the Bitcoin testnet.
|
236 |
+
// See https://scrypt.io/docs/bitcoin-basics/bsv/#private-keys
|
237 |
+
const privateKey = bsv.PrivateKey.fromWIF(process.env.PRIVATE_KEY || '')
|
238 |
+
|
239 |
+
// Prepare signer.
|
240 |
+
// See https://scrypt.io/docs/how-to-deploy-and-call-a-contract/#prepare-a-signer-and-provider
|
241 |
+
const signer = new TestWallet(privateKey, new DefaultProvider({
|
242 |
+
network: bsv.Networks.testnet
|
243 |
+
}))
|
244 |
+
|
245 |
+
async function main() {
|
246 |
+
await Voting.compile()
|
247 |
+
|
248 |
+
const candidateNames: FixedArray<Name, typeof N> = [
|
249 |
+
toByteString('iPhone', true),
|
250 |
+
toByteString('Android', true)
|
251 |
+
]
|
252 |
+
|
253 |
+
const instance = new Voting(
|
254 |
+
candidateNames
|
255 |
+
)
|
256 |
+
|
257 |
+
// Connect to a signer.
|
258 |
+
await instance.connect(signer)
|
259 |
+
|
260 |
+
// Contract deployment.
|
261 |
+
const amount = 1
|
262 |
+
const deployTx = await instance.deploy(amount)
|
263 |
+
console.log('Voting contract deployed: ', deployTx.id)
|
264 |
+
}
|
265 |
+
|
266 |
+
main()
|
267 |
+
```
|
268 |
+
|
269 |
+
Before deploying the contract, we need to create a `.env` file and save your private key in the `PRIVATE_KEY` environment variable.
|
270 |
+
|
271 |
+
```
|
272 |
+
PRIVATE_KEY=xxxxx
|
273 |
+
```
|
274 |
+
|
275 |
+
If you don't have a private key, you can follow [this guide](../../how-to-deploy-and-call-a-contract/faucet) to generate one using Sensilet wallet, then fund the private key's address with our [faucet](https://scrypt.io/faucet/).
|
276 |
+
|
277 |
+
Run the following command to deploy the contract.
|
278 |
+
|
279 |
+
```bash
|
280 |
+
npm run deploy:contract
|
281 |
+
```
|
282 |
+
|
283 |
+
After success, you will see an output similar to the following:
|
284 |
+
|
285 |
+

|
286 |
+
|
287 |
+
#### Contract ID
|
288 |
+
|
289 |
+
Your can get the deployed contract's ID: the TXID and the output index where the contract is located.
|
290 |
+
```js
|
291 |
+
const contract_id = {
|
292 |
+
/** the deployment transaction id */
|
293 |
+
txId: "6751b645e1579e8e6201e3c59b900ad58e59868aa5e4ee89359d3f8ca1d66c8a",
|
294 |
+
/** the output index */
|
295 |
+
outputIndex: 0,
|
296 |
+
};
|
297 |
+
```
|
298 |
+
|
299 |
+
### Verify
|
300 |
+
|
301 |
+
After a successful deployment of a smart contract, you can verify the deployed contract script:
|
302 |
+
|
303 |
+
```sh
|
304 |
+
npm run verify:contract
|
305 |
+
```
|
306 |
+
|
307 |
+
Upon execution, the designated contract code undergoes verification on sCrypt's servers. If successful, the outcome will be [displayed on WoC](https://test.whatsonchain.com/script/cecb4f8799913df3e5af50bc81a24e3fef3216a92452d27cd97dcd7ccbce1f1b), under the "sCrypt" tab. See the ["How to Verify a Contract"](../how-to-verify-a-contract.md) page for more details.
|
308 |
+
|
309 |
+
### Load Contract Artifact
|
310 |
+
|
311 |
+
Before writing the front-end code, we need to load the contract artifact in `src\index.tsx`.
|
312 |
+
|
313 |
+
```ts
|
314 |
+
import { Voting } from './contracts/voting';
|
315 |
+
var artifact = require('../artifacts/src/contracts/voting.json');
|
316 |
+
Voting.loadArtifact(artifact);
|
317 |
+
```
|
318 |
+
|
319 |
+
### Integrate Wallet
|
320 |
+
|
321 |
+
Use `requestAuth` method of `signer` to request access to the wallet.
|
322 |
+
|
323 |
+
```ts
|
324 |
+
// request authentication
|
325 |
+
const { isAuthenticated, error } = await signer.requestAuth();
|
326 |
+
if (!isAuthenticated) {
|
327 |
+
// something went wrong, throw an Error with `error` message
|
328 |
+
throw new Error(error);
|
329 |
+
}
|
330 |
+
|
331 |
+
// authenticated
|
332 |
+
// ...
|
333 |
+
```
|
334 |
+
|
335 |
+
### Integrate sCrypt Service
|
336 |
+
|
337 |
+
To interacte with the voting contract, we need to create a contract instance representing the latest state of the contract on chain. When both Alice and Bob vote on the webpage, we need to ensure that their contract instances are always up to date. After Alice votes, we have to notify Bob that the state of the contract has changed and synchronize his local contract instance to the latest state on chain.
|
338 |
+
|
339 |
+
Fortunately,`sCrypt` provides such infrastructure service, which abstracts away all the common complexities of communicating with the blockchain, so we do not have to track the contract state, which could be computationally demanding as blockchain grows. We can instead focus on our application's business logic.
|
340 |
+
|
341 |
+
To use it, we first have to initialize it according to [this guide](../advanced/how-to-integrate-scrypt-service.md).
|
342 |
+
|
343 |
+
```ts
|
344 |
+
Scrypt.init({
|
345 |
+
apiKey: 'YOUR_API_KEY',
|
346 |
+
network: bsv.Networks.testnet
|
347 |
+
})
|
348 |
+
```
|
349 |
+
|
350 |
+
### Connect Signer to `ScryptProvider`
|
351 |
+
|
352 |
+
It's required to connect your signer to `ScryptProvider` when using sCrypt service.
|
353 |
+
|
354 |
+
```ts
|
355 |
+
const provider = new ScryptProvider();
|
356 |
+
const signer = new SensiletSigner(provider);
|
357 |
+
|
358 |
+
signerRef.current = signer;
|
359 |
+
```
|
360 |
+
|
361 |
+
### Fetch Latest Contract Instance
|
362 |
+
|
363 |
+
We can fetch a contract's latest instance by calling the `Scrypt.contractApi.getLatestInstance()` using its [contract ID](#contract-id). With this instance, we can easily read a contract's properties to display to the user on the webpage, or update the contract state by calling its public method as [before](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#contract-call) when the user votes for a candidate.
|
364 |
+
|
365 |
+
```ts
|
366 |
+
function App() {
|
367 |
+
const [votingContract, setContract] = useState<Voting>();
|
368 |
+
const [error, setError] = React.useState("");
|
369 |
+
|
370 |
+
// ...
|
371 |
+
|
372 |
+
async function fetchContract() {
|
373 |
+
try {
|
374 |
+
const instance = await Scrypt.contractApi.getLatestInstance(
|
375 |
+
Voting,
|
376 |
+
contract_id
|
377 |
+
);
|
378 |
+
setContract(instance);
|
379 |
+
} catch (error: any) {
|
380 |
+
console.error("fetchContract error: ", error);
|
381 |
+
setError(error.message);
|
382 |
+
}
|
383 |
+
}
|
384 |
+
|
385 |
+
// ...
|
386 |
+
}
|
387 |
+
```
|
388 |
+
|
389 |
+
### Read contract state
|
390 |
+
|
391 |
+
With the contract instance, we can read its lastest state and render it.
|
392 |
+
|
393 |
+
```ts
|
394 |
+
function byteString2utf8(b: ByteString) {
|
395 |
+
return Buffer.from(b, "hex").toString("utf8");
|
396 |
+
}
|
397 |
+
|
398 |
+
function App() {
|
399 |
+
// ...
|
400 |
+
|
401 |
+
return (
|
402 |
+
<div className="App">
|
403 |
+
<header className="App-header">
|
404 |
+
<h2>What's your favorite phone?</h2>
|
405 |
+
</header>
|
406 |
+
<TableContainer
|
407 |
+
component={Paper}
|
408 |
+
variant="outlined"
|
409 |
+
style={{ width: 1200, height: "80vh", margin: "auto" }}
|
410 |
+
>
|
411 |
+
<Table>
|
412 |
+
<TableHead>
|
413 |
+
<TableRow>
|
414 |
+
<TableCell align="center">Iphone</TableCell>
|
415 |
+
<TableCell align="center">Android</TableCell>
|
416 |
+
</TableRow>
|
417 |
+
</TableHead>
|
418 |
+
<TableBody>
|
419 |
+
<TableRow>
|
420 |
+
<TableCell align="center">
|
421 |
+
<Box>
|
422 |
+
<Box
|
423 |
+
sx={{
|
424 |
+
height: 200,
|
425 |
+
}}
|
426 |
+
component="img"
|
427 |
+
alt={"iphone"}
|
428 |
+
src={`${process.env.PUBLIC_URL}/${"iphone"}.png`}
|
429 |
+
/>
|
430 |
+
</Box>
|
431 |
+
</TableCell>
|
432 |
+
<TableCell align="center">
|
433 |
+
<Box>
|
434 |
+
<Box
|
435 |
+
sx={{
|
436 |
+
height: 200,
|
437 |
+
}}
|
438 |
+
component="img"
|
439 |
+
alt={"android"}
|
440 |
+
src={`${process.env.PUBLIC_URL}/${"android"}.png`}
|
441 |
+
/>
|
442 |
+
</Box>
|
443 |
+
</TableCell>
|
444 |
+
</TableRow>
|
445 |
+
<TableRow>
|
446 |
+
<TableCell align="center">
|
447 |
+
<Box>
|
448 |
+
<Typography variant={"h1"} >
|
449 |
+
{votingContract?.candidates[0].votesReceived.toString()}
|
450 |
+
</Typography>
|
451 |
+
<Button
|
452 |
+
variant="text"
|
453 |
+
onClick={voting}
|
454 |
+
name={votingContract?.candidates[0].name}
|
455 |
+
>
|
456 |
+
π
|
457 |
+
</Button>
|
458 |
+
</Box>
|
459 |
+
</TableCell>
|
460 |
+
|
461 |
+
<TableCell align="center">
|
462 |
+
<Divider orientation="vertical" flexItem />
|
463 |
+
<Box>
|
464 |
+
<Typography variant={"h1"}>
|
465 |
+
{votingContract?.candidates[1].votesReceived.toString()}
|
466 |
+
</Typography>
|
467 |
+
<Button
|
468 |
+
variant="text"
|
469 |
+
onClick={voting}
|
470 |
+
name={votingContract?.candidates[1].name}
|
471 |
+
>
|
472 |
+
π
|
473 |
+
</Button>
|
474 |
+
</Box>
|
475 |
+
</TableCell>
|
476 |
+
</TableRow>
|
477 |
+
</TableBody>
|
478 |
+
</Table>
|
479 |
+
</TableContainer>
|
480 |
+
<Footer />
|
481 |
+
<Snackbar
|
482 |
+
open={error !== ""}
|
483 |
+
autoHideDuration={6000}
|
484 |
+
onClose={handleClose}
|
485 |
+
>
|
486 |
+
<Alert severity="error">{error}</Alert>
|
487 |
+
</Snackbar>
|
488 |
+
|
489 |
+
<Snackbar
|
490 |
+
open={success.candidate !== "" && success.txId !== ""}
|
491 |
+
autoHideDuration={6000}
|
492 |
+
onClose={handleSuccessClose}
|
493 |
+
>
|
494 |
+
<Alert severity="success">
|
495 |
+
{" "}
|
496 |
+
<Link
|
497 |
+
href={`https://test.whatsonchain.com/tx/${success.txId}`}
|
498 |
+
target="_blank"
|
499 |
+
rel="noreferrer"
|
500 |
+
>
|
501 |
+
{`"${byteString2utf8(success.candidate)}" got one vote, tx: ${
|
502 |
+
success.txId
|
503 |
+
}`}
|
504 |
+
</Link>
|
505 |
+
</Alert>
|
506 |
+
</Snackbar>
|
507 |
+
</div>
|
508 |
+
);
|
509 |
+
}
|
510 |
+
```
|
511 |
+
|
512 |
+
### Update Contract State
|
513 |
+
|
514 |
+
To update the contract's state, we need to call its public method. We create a function `voting()` to handle the voting event triggered by a user.
|
515 |
+
|
516 |
+
Calling a contract public method is [the same as before](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#contract-call).
|
517 |
+
|
518 |
+
```ts
|
519 |
+
async function voting(e: any) {
|
520 |
+
// ...
|
521 |
+
|
522 |
+
const signer = signerRef.current as SensiletSigner;
|
523 |
+
|
524 |
+
if (votingContract && signer) {
|
525 |
+
const { isAuthenticated, error } = await signer.requestAuth();
|
526 |
+
if (!isAuthenticated) {
|
527 |
+
throw new Error(error);
|
528 |
+
}
|
529 |
+
|
530 |
+
await votingContract.connect(signer);
|
531 |
+
|
532 |
+
// create the next instance from the current
|
533 |
+
const nextInstance = votingContract.next();
|
534 |
+
|
535 |
+
const candidateName = e.target.name;
|
536 |
+
|
537 |
+
// update state
|
538 |
+
nextInstance.increaseVotesReceived(candidateName);
|
539 |
+
|
540 |
+
// call the method of current instance to apply the updates on chain
|
541 |
+
votingContract.methods
|
542 |
+
.vote(candidateName, {
|
543 |
+
next: {
|
544 |
+
instance: nextInstance,
|
545 |
+
balance: votingContract.balance,
|
546 |
+
},
|
547 |
+
})
|
548 |
+
.then((result) => {
|
549 |
+
console.log(`Voting call tx: ${result.tx.id}`);
|
550 |
+
})
|
551 |
+
.catch((e) => {
|
552 |
+
setError(e.message);
|
553 |
+
fetchContract();
|
554 |
+
console.error("call error: ", e);
|
555 |
+
});
|
556 |
+
}
|
557 |
+
}
|
558 |
+
```
|
559 |
+
|
560 |
+
If successful, you will see the following log in the `console`:
|
561 |
+
|
562 |
+
```
|
563 |
+
Voting call tx: fc8b3d03b8fa7469d66a165b017fe941fa8ab59c0979457cef2b6415d659e3f7
|
564 |
+
```
|
565 |
+
|
566 |
+
### Subscribe to Contract Event
|
567 |
+
|
568 |
+
So far, we have a fully working app. However, there is a slight problem. When Alice clicks on the like button for a candadate in her browser, the candidate's vote count in Bob's browser does not increase, unless he manually refreshes.
|
569 |
+
We need a way to listen to contract event.
|
570 |
+
|
571 |
+
We call `Scrypt.contractApi.subscribe(options: SubscribeOptions<T>, cb: (e: ContractCalledEvent<T>) => void): SubScription` to subscribe to events that the contract has been called. When a contract gets called and updated, we refresh the UI in real time, re-render all the content on the page and show the updated vote count.
|
572 |
+
|
573 |
+
The subscribe function takes 2 parameters:
|
574 |
+
|
575 |
+
1. `options: SubscribeOptions<T>`: it includes a contract class, a contract ID, and a optional list of method names monitored.
|
576 |
+
|
577 |
+
```ts
|
578 |
+
interface SubscribeOptions<T> {
|
579 |
+
clazz: new (...args: any) => T;
|
580 |
+
id: ContractId;
|
581 |
+
methodNames?: Array<string>;
|
582 |
+
}
|
583 |
+
```
|
584 |
+
|
585 |
+
If `methodNames` is set, you will be notified only when public functions in the list are called. Otherwise, you will be notified when ANY public function is called.
|
586 |
+
|
587 |
+
2. `callback: (event: ContractCalledEvent<T>) => void`: a callback funciton upon receiving notifications.
|
588 |
+
|
589 |
+
`ContractCalledEvent<T>` contains the relevant information when the contract is called, such as the public function name and function arguments when the call occurs.
|
590 |
+
|
591 |
+
```ts
|
592 |
+
export interface ContractCalledEvent<T> {
|
593 |
+
/** name of public function */
|
594 |
+
methodName: string;
|
595 |
+
/** public function arguments */
|
596 |
+
args: SupportedParamType[];
|
597 |
+
/** transaction where contract is called from */
|
598 |
+
tx: bsv.Transaction;
|
599 |
+
/**
|
600 |
+
* If a stateful contract is called, `nexts` contains the contract instance containing the new state generated by this call.
|
601 |
+
* If a stateless contract is called, `nexts` is empty.
|
602 |
+
*/
|
603 |
+
nexts: Array<T>;
|
604 |
+
}
|
605 |
+
```
|
606 |
+
|
607 |
+
The code to subscribe to contract events is as follows.
|
608 |
+
|
609 |
+
```ts
|
610 |
+
useEffect(() => {
|
611 |
+
const provider = new ScryptProvider();
|
612 |
+
const signer = new SensiletSigner(provider);
|
613 |
+
|
614 |
+
signerRef.current = signer;
|
615 |
+
|
616 |
+
fetchContract();
|
617 |
+
|
618 |
+
// subscribe by contract_id
|
619 |
+
const subscription = Scrypt.contractApi.subscribe({
|
620 |
+
clazz: Voting,
|
621 |
+
id: contract_id
|
622 |
+
}, (event: ContractCalledEvent<Voting>) => {
|
623 |
+
// update the contract instance
|
624 |
+
setSuccess({
|
625 |
+
txId: event.tx.id,
|
626 |
+
candidate: event.args[0] as ByteString,
|
627 |
+
});
|
628 |
+
setContract(event.nexts[0]);
|
629 |
+
});
|
630 |
+
|
631 |
+
return () => {
|
632 |
+
// unsubscribe
|
633 |
+
subscription.unsubscribe();
|
634 |
+
};
|
635 |
+
}, []);
|
636 |
+
```
|
637 |
+
|
638 |
+
### Deploy to GitHub Pages
|
639 |
+
|
640 |
+
After pushing the frontend project to your GitHub account, it's easy to [publish a website with GitHub Pages](https://create-react-app.dev/docs/deployment/#github-pages), so that users can interact with your dApp with the browser.
|
641 |
+
|
642 |
+
#### Step 1. Add `homepage` to `package.json`
|
643 |
+
|
644 |
+
Open your `package.json` and add a `homepage` field for your project.
|
645 |
+
|
646 |
+
```json
|
647 |
+
{
|
648 |
+
"name": "voting",
|
649 |
+
"homepage": "https://[YOUR-GITHUB-USERNAME].github.io/[YOUR-REPO-NAME]"
|
650 |
+
...
|
651 |
+
}
|
652 |
+
```
|
653 |
+
|
654 |
+

|
655 |
+
|
656 |
+
For example, our demo repo is at https://github.com/sCrypt-Inc/voting, so we set
|
657 |
+
|
658 |
+
```
|
659 |
+
https://sCrypt-Inc.github.io/voting
|
660 |
+
```
|
661 |
+
|
662 |
+
as the homepage, where `sCrypt-Inc` is our GitHub username, and `voting` is the repo name.
|
663 |
+
|
664 |
+
#### Step 2. Install `gh-pages` and add `scripts` in `package.json`
|
665 |
+
|
666 |
+
Run the following command to install the dependency.
|
667 |
+
|
668 |
+
```sh
|
669 |
+
npm install --save gh-pages
|
670 |
+
```
|
671 |
+
|
672 |
+
Then add two scripts in `package.json`.
|
673 |
+
|
674 |
+
```json
|
675 |
+
"scripts": {
|
676 |
+
"predeploy": "npm run build",
|
677 |
+
"deploy": "gh-pages -d build",
|
678 |
+
...
|
679 |
+
},
|
680 |
+
```
|
681 |
+
|
682 |
+

|
683 |
+
|
684 |
+
:::note
|
685 |
+
The `predeploy` script will run automatically before `deploy` is run.
|
686 |
+
:::
|
687 |
+
|
688 |
+
#### Step 3. Deploy the site
|
689 |
+
|
690 |
+
Run the following command to deploy the website.
|
691 |
+
|
692 |
+
```sh
|
693 |
+
npm run deploy
|
694 |
+
```
|
695 |
+
|
696 |
+
#### Step 4. Update GitHub project settings
|
697 |
+
|
698 |
+
After running the `deploy` script, don't forget to update your GitHub project settings to use `gh-pages` branch. Go to `Settings --> Code and automation/Pages`, and select `gh-pages` as the branch used by the GitHub Pages site.
|
699 |
+
|
700 |
+

|
701 |
+
|
702 |
+
### Conclusion
|
703 |
+
|
704 |
+
Congratulations! You have successfully completed a fullstack voting dapp fully on Bitcoin.
|
705 |
+
|
706 |
+
The repo is [here](https://github.com/sCrypt-Inc/voting). And an online example is [here](http://classic.scrypt.io/voting).
|
zkp.md
ADDED
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
sidebar_position: 5
|
3 |
+
---
|
4 |
+
|
5 |
+
# Tutorial 5: Zero Knowledge Proofs
|
6 |
+
|
7 |
+
## Overview
|
8 |
+
|
9 |
+
In this tutorial we will go over how to create a zero-knowledge proof (ZKP) and verify it on Bitcoin using sCrypt.
|
10 |
+
|
11 |
+
### What are zk-SNARKS?
|
12 |
+
|
13 |
+
SNARK (zero-knowledge Succinct Non-interactive ARguments of Knowledge) is a type of ZKP that is amenable for blockchains. The generated proof is βsuccinctβ and βnon-interactiveβ: a proof is only a few hundred bytes and can be verified in constant time and within a few milliseconds, without needing to ask additional questions of the prover. Together, these properties make zk-SNARK especially suitable for blockchains, where on-chain storage and computation can be expensive and senders often go offline after sending a transaction.
|
14 |
+
|
15 |
+
A proof is constructed off-chain by a prover who generates the proof using a secret input (often referred to as the "witness") and a public input. The prover can then use this proof as an input for an sCrypt smart contract, which can verify the validity of the proof using a verification key and the public input.
|
16 |
+
|
17 |
+

|
18 |
+
|
19 |
+
[Credit: altoros](https://www.altoros.com/blog/securing-a-blockchain-with-a-noninteractive-zero-knowledge-proof/)
|
20 |
+
|
21 |
+
|
22 |
+
There are many tools for creating such proofs, [ZoKrates](https://github.com/sCrypt-Inc/zokrates) and [SnarkJS](https://github.com/sCrypt-Inc/snarkjs) are among the most popular.
|
23 |
+
|
24 |
+
In this example we will use ZoKrates. It provides a python-like higher-level language for developers to code the computational problem they want to prove.
|
25 |
+
|
26 |
+
For a more comprehensive explanation of zk-SNARKS and how they work, we recommend reading [this blog post](https://xiaohuiliu.medium.com/zk-snarks-on-bitcoin-239d96d182bd).
|
27 |
+
|
28 |
+
## Install ZoKrates
|
29 |
+
|
30 |
+
Run the following command to install [released binaries](https://github.com/sCrypt-Inc/zokrates/releases):
|
31 |
+
|
32 |
+
```sh
|
33 |
+
curl -Ls https://scrypt.io/scripts/setup-zokrates.sh | sh -s -
|
34 |
+
```
|
35 |
+
|
36 |
+
or build from source:
|
37 |
+
|
38 |
+
```sh
|
39 |
+
git clone https://github.com/sCrypt-Inc/zokrates
|
40 |
+
cd ZoKrates
|
41 |
+
cargo +nightly build -p zokrates_cli --release
|
42 |
+
cd target/release
|
43 |
+
```
|
44 |
+
|
45 |
+
## ZoKrates Workflow
|
46 |
+
|
47 |
+
### 1. Design a circuit
|
48 |
+
|
49 |
+
Create a new ZoKrates file named `factor.zok` with the following content:
|
50 |
+
|
51 |
+
```python
|
52 |
+
// p, q are the factors of n
|
53 |
+
def main(private field p, private field q, field n) {
|
54 |
+
assert(p * q == n);
|
55 |
+
assert(p > 1);
|
56 |
+
assert(q > 1);
|
57 |
+
return;
|
58 |
+
}
|
59 |
+
```
|
60 |
+
|
61 |
+
This simple circuit/program proves one knows a factorization of an integer `n` into two integers, without revealing the factors. The circuit has two private inputs named `p` and `q` and one public input named `n`.
|
62 |
+
|
63 |
+
|
64 |
+
### 2. Compile the circuit
|
65 |
+
|
66 |
+
Compile the circuit with the following command:
|
67 |
+
|
68 |
+
```sh
|
69 |
+
zokrates compile -i factor.zok
|
70 |
+
```
|
71 |
+
|
72 |
+
This generates two files that encode the circuit in binary and human-readable format.
|
73 |
+
|
74 |
+
### 3. Setup
|
75 |
+
|
76 |
+
This generates a proving key and a verification key for this circuit.
|
77 |
+
|
78 |
+
```sh
|
79 |
+
zokrates setup
|
80 |
+
```
|
81 |
+
|
82 |
+
### 4. Calculate a witness
|
83 |
+
|
84 |
+
A proof attests that a prover knows some secret/private information that satisfies the original program. This secret information is called witness. In the following example, `7` and `13` are the witnesses, as they are factors of `91`.
|
85 |
+
|
86 |
+
```sh
|
87 |
+
zokrates compute-witness -a 7 13 91
|
88 |
+
```
|
89 |
+
|
90 |
+
A file named `witness` is generated.
|
91 |
+
|
92 |
+
### 5. Creating a proof
|
93 |
+
|
94 |
+
The following command produces a proof, using both the proving key and the witness:
|
95 |
+
|
96 |
+
```sh
|
97 |
+
zokrates generate-proof
|
98 |
+
```
|
99 |
+
|
100 |
+
The resulting file `proof.json` looks like the following:
|
101 |
+
|
102 |
+
```json
|
103 |
+
{
|
104 |
+
"scheme": "g16",
|
105 |
+
"curve": "bn128",
|
106 |
+
"proof": {
|
107 |
+
"a": [
|
108 |
+
"0x0a7ea3ca37865347396645d017c7623431d13103e9107c937d722e5da15f352b",
|
109 |
+
"0x040c202ba8fa153f84af8dabc2ca40ff534f54efeb3271acc04a70c41afd079b"
|
110 |
+
],
|
111 |
+
"b": [
|
112 |
+
[
|
113 |
+
"0x0ec1e4faea792762de35dcfd0da0e6859ce491cafad455c334d2c72cb8b24550",
|
114 |
+
"0x0985ef1d036b41d44376c1d42ff803b7cab9f9d4cf5bd75298e0fab2d109f096"
|
115 |
+
],
|
116 |
+
[
|
117 |
+
"0x265151afd8626b4c72dfefb86bac2b63489423d6cf895ed9fa186548b0b9e3f3",
|
118 |
+
"0x301f2b356621408e037649d0f5b4ad5f4b2333f58453791cc24f07d5673349bf"
|
119 |
+
]
|
120 |
+
],
|
121 |
+
"c": [
|
122 |
+
"0x2b75a257d68763100ca11afb3beae511732c1cd1d3f1ce1804cbc0c26043cb6b",
|
123 |
+
"0x2f80c706b58482eec9e759fce805585595a76c27e37b67af3463414246fbabbd"
|
124 |
+
]
|
125 |
+
},
|
126 |
+
"inputs": [
|
127 |
+
"0x000000000000000000000000000000000000000000000000000000000000005b"
|
128 |
+
]
|
129 |
+
}
|
130 |
+
```
|
131 |
+
|
132 |
+
### 6. Export an sCrypt verifier
|
133 |
+
|
134 |
+
Using our version of ZoKrates, we can export a project template, which will contain a verifier for our circuit. Simply run the following command:
|
135 |
+
|
136 |
+
```sh
|
137 |
+
zokrates export-verifier-scrypt
|
138 |
+
```
|
139 |
+
|
140 |
+
This will create a directory named `verifier`, containing the project. Let's set it up. Run the following:
|
141 |
+
|
142 |
+
```sh
|
143 |
+
cd verifier && git init && npm i
|
144 |
+
```
|
145 |
+
|
146 |
+
Now the verifier is ready to be used. In the following section we will go over the code and show how to use it.
|
147 |
+
|
148 |
+
|
149 |
+
### 7. Run the sCrypt Verifier
|
150 |
+
|
151 |
+
In the generated project, let's open the file `src/contracts/verifier.ts`. This file contains an sCrypt smart contract, named `Verifier`, which can be unlocked by providing a valid ZK proof.
|
152 |
+
|
153 |
+
Under the hood it uses the `SNARK` library from `src/contracts/snark.ts`. This file includes an elliptic curve implementation along with a library that implements pairings over that elliptic curve and lastly the implementation of the proof verification algorithm. In our example the [`BN-256` elliptic curve](https://hackmd.io/@jpw/bn254) is being used along with the [`Groth-16` proof system](https://eprint.iacr.org/2016/260.pdf)..
|
154 |
+
|
155 |
+
Let's take a look at the implementation of `Verifier`:
|
156 |
+
|
157 |
+
```ts
|
158 |
+
export class Verifier extends SmartContract {
|
159 |
+
|
160 |
+
@prop()
|
161 |
+
vk: VerifyingKey
|
162 |
+
|
163 |
+
@prop()
|
164 |
+
publicInputs: FixedArray<bigint, typeof N_PUB_INPUTS>,
|
165 |
+
|
166 |
+
constructor(
|
167 |
+
vk: VerifyingKey,
|
168 |
+
publicInputs: FixedArray<bigint, typeof N_PUB_INPUTS>,
|
169 |
+
) {
|
170 |
+
super(...arguments)
|
171 |
+
this.vk = vk
|
172 |
+
this.publicInputs = publicInputs
|
173 |
+
}
|
174 |
+
|
175 |
+
@method()
|
176 |
+
public verifyProof(
|
177 |
+
proof: Proof
|
178 |
+
) {
|
179 |
+
assert(SNARK.verify(this.vk, this.publicInputs, proof))
|
180 |
+
}
|
181 |
+
|
182 |
+
}
|
183 |
+
```
|
184 |
+
|
185 |
+
As we can see, the contract has two properties, namely the verification key and the value(s) of the public inputs to our ZK program.
|
186 |
+
|
187 |
+
The contract also has a public method named `verifyProof`. As the name implies it verifies a ZK proof and can be unlocked by a valid one. The proof is passed as a parameter. The method calls the proof verification function:
|
188 |
+
|
189 |
+
```ts
|
190 |
+
SNARK.verify(this.vk, this.publicInputs, proof)
|
191 |
+
```
|
192 |
+
|
193 |
+
The function takes as parameters the verification key, the public inputs and the proof. It's important to note that the proof is cryptographically tied to the verification key and thus must be a proof about the correct ZoKrates program (`factor.zok`).
|
194 |
+
|
195 |
+
The generated project will also contain a deployment script `deploy.ts`. Let's take a look at the code:
|
196 |
+
|
197 |
+
```ts
|
198 |
+
async function main() {
|
199 |
+
await Verifier.compile()
|
200 |
+
|
201 |
+
// TODO: Adjust the amount of satoshis locked in the smart contract:
|
202 |
+
const amount = 100
|
203 |
+
|
204 |
+
// TODO: Insert public input values here:
|
205 |
+
const publicInputs: FixedArray<bigint, typeof N_PUB_INPUTS> = [ 0n ]
|
206 |
+
|
207 |
+
let verifier = new Verifier(
|
208 |
+
prepareVerifyingKey(VERIFYING_KEY_DATA),
|
209 |
+
publicInputs
|
210 |
+
)
|
211 |
+
|
212 |
+
// Connect to a signer.
|
213 |
+
await verifier.connect(getDefaultSigner())
|
214 |
+
|
215 |
+
// Deploy:
|
216 |
+
const deployTx = await verifier.deploy(amount)
|
217 |
+
console.log('Verifier contract deployed: ', deployTx.id)
|
218 |
+
}
|
219 |
+
|
220 |
+
main()
|
221 |
+
```
|
222 |
+
|
223 |
+
We can observe that we need to adjust two things. First, we need to set the amount of satoshis we will lock into the deployed smart contract. The second thing is the public input value, i.e. the product of the secret factors. Let's set it to the value `91`:
|
224 |
+
|
225 |
+
```ts
|
226 |
+
const publicInputs: FixedArray<bigint, typeof N_PUB_INPUTS> = [ 91n ]
|
227 |
+
```
|
228 |
+
|
229 |
+
Note also, that ZoKrates already provided us with the values of the verification key, that we created during the setup phase.
|
230 |
+
|
231 |
+
Now, we can build and deploy the contract. Simply run:
|
232 |
+
|
233 |
+
```sh
|
234 |
+
npm run deploy
|
235 |
+
```
|
236 |
+
|
237 |
+
The first time you run the command, it will ask you to fund a testnet address. You can fund it using [our faucet](https://scrypt.io/faucet/).
|
238 |
+
|
239 |
+
After a successful run you should see something like the following:
|
240 |
+
|
241 |
+
```
|
242 |
+
Verifier contract deployed: 2396a4e52555cdc29795db281d17de423697bd5cbabbcb756cb14cea8e947235
|
243 |
+
```
|
244 |
+
|
245 |
+
The smart contract is now deployed and can be unlocked using a valid proof, that proves the knowledge of the factors for the integer `91`. You can see [the transaction](https://test.whatsonchain.com/tx/2396a4e52555cdc29795db281d17de423697bd5cbabbcb756cb14cea8e947235) using a block explorer.
|
246 |
+
|
247 |
+
Let's call the deployed contract. Let's create a file named `call.ts` with the following content:
|
248 |
+
|
249 |
+
```ts
|
250 |
+
import { DefaultProvider } from 'scrypt-ts'
|
251 |
+
import { parseProofFile } from './src/util'
|
252 |
+
import { Verifier } from './src/contracts/verifier'
|
253 |
+
import { Proof } from './src/contracts/snark'
|
254 |
+
import { getDefaultSigner } from './tests/utils/helper'
|
255 |
+
import { PathLike } from 'fs'
|
256 |
+
|
257 |
+
export async function call(txId: string, proofPath: PathLike) {
|
258 |
+
await Verifier.compile()
|
259 |
+
|
260 |
+
// Fetch TX via provider and reconstruct contract instance
|
261 |
+
const provider = new DefaultProvider()
|
262 |
+
const tx = await provider.getTransaction(txId)
|
263 |
+
const verifier = Verifier.fromTx(tx, 0)
|
264 |
+
|
265 |
+
// Connect signer
|
266 |
+
await verifier.connect(getDefaultSigner())
|
267 |
+
|
268 |
+
// Parse proof.json
|
269 |
+
const proof: Proof = parseProofFile(proofPath)
|
270 |
+
|
271 |
+
// Call verifyProof()
|
272 |
+
const { tx: callTx } = await verifier.methods.verifyProof(
|
273 |
+
proof
|
274 |
+
)
|
275 |
+
console.log('Verifier contract unlocked: ', callTx.id)
|
276 |
+
}
|
277 |
+
|
278 |
+
(async () => {
|
279 |
+
await call('2396a4e52555cdc29795db281d17de423697bd5cbabbcb756cb14cea8e947235', '../proof.json')
|
280 |
+
})()
|
281 |
+
```
|
282 |
+
|
283 |
+
The function `call` will create the contract instance from the passed [TXID](https://wiki.bitcoinsv.io/index.php/TXID) and call its `verifyProof` method. The proof gets parsed from `proof.json`, which we already created in the section above.
|
284 |
+
|
285 |
+
Let's unlock our contract by running the following command:
|
286 |
+
```
|
287 |
+
npx ts-node call.ts
|
288 |
+
```
|
289 |
+
|
290 |
+
If everything goes as expected, we have now unlocked the verifier smart contract. You'll see an output similar to the following:
|
291 |
+
|
292 |
+
```
|
293 |
+
Verifier contract unlocked: 30127e0c340878d3fb7c165e2d082267eef2c8df79b5cf750896ef565ca7651d
|
294 |
+
```
|
295 |
+
|
296 |
+
Take a look at it using [a block explorer](https://test.whatsonchain.com/tx/30127e0c340878d3fb7c165e2d082267eef2c8df79b5cf750896ef565ca7651d).
|
297 |
+
|
298 |
+
## Conclusion
|
299 |
+
|
300 |
+
Congratulations! You have successfully created a zk-SNARK and verified it on-chain!
|
301 |
+
|
302 |
+
If you want to learn how you can integrate zk-SNARKS into a fully fledged Bitcoin web application, take a look at our free [course](https://academy.scrypt.io/en/courses/Build-a-zkSNARK-based-Battleship-Game-on-Bitcoin-64187ae0d1a6cb859d18d72a), which will teach you how to create a ZK Battleship game.
|
303 |
+
Additionally, it teaches you to use [snarkjs/circom](https://github.com/sCrypt-Inc/snarkjs).
|
304 |
+
|
305 |
+
To know more about ZKP, you can refer to [this awesome list](https://github.com/sCrypt-Inc/awesome-zero-knowledge-proofs).
|