ecash-agora 0.1.1-rc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +8 -0
- package/.nycrc +4 -0
- package/README.md +122 -0
- package/agora.py +771 -0
- package/dist/ad.d.ts +15 -0
- package/dist/ad.d.ts.map +1 -0
- package/dist/ad.js +111 -0
- package/dist/ad.js.map +1 -0
- package/dist/agora.d.ts +178 -0
- package/dist/agora.d.ts.map +1 -0
- package/dist/agora.js +432 -0
- package/dist/agora.js.map +1 -0
- package/dist/consts.d.ts +5 -0
- package/dist/consts.d.ts.map +1 -0
- package/dist/consts.js +9 -0
- package/dist/consts.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/oneshot.d.ts +34 -0
- package/dist/oneshot.d.ts.map +1 -0
- package/dist/oneshot.js +204 -0
- package/dist/oneshot.js.map +1 -0
- package/dist/partial.d.ts +256 -0
- package/dist/partial.d.ts.map +1 -0
- package/dist/partial.js +955 -0
- package/dist/partial.js.map +1 -0
- package/eslint.config.js +16 -0
- package/package.json +52 -0
- package/tests/oneshot.test.ts +569 -0
- package/tests/partial-helper-alp.ts +131 -0
- package/tests/partial-helper-slp.ts +154 -0
- package/tests/partial.alp.bigsats.test.ts +694 -0
- package/tests/partial.alp.test.ts +586 -0
- package/tests/partial.slp.bigsats.test.ts +681 -0
- package/tests/partial.slp.test.ts +630 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +19 -0
package/dist/partial.js
ADDED
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
// Copyright (c) 2024 The Bitcoin developers
|
|
2
|
+
// Distributed under the MIT software license, see the accompanying
|
|
3
|
+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
4
|
+
import { ALL_ANYONECANPAY_BIP143, ALL_BIP143, alpSend, DEFAULT_DUST_LIMIT, emppScript, flagSignature, OP_0, OP_0NOTEQUAL, OP_1, OP_12, OP_2, OP_2DUP, OP_2OVER, OP_2SWAP, OP_3, OP_3DUP, OP_8, OP_9, OP_ADD, OP_BIN2NUM, OP_CAT, OP_CHECKDATASIGVERIFY, OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CODESEPARATOR, OP_DIV, OP_DROP, OP_DUP, OP_ELSE, OP_ENDIF, OP_EQUAL, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_GREATERTHANOREQUAL, OP_HASH160, OP_HASH256, OP_IF, OP_MOD, OP_NIP, OP_NOTIF, OP_NUM2BIN, OP_OVER, OP_PICK, OP_PUSHDATA1, OP_REVERSEBYTES, OP_ROT, OP_SHA256, OP_SIZE, OP_SPLIT, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_TUCK, OP_VERIFY, pushBytesOp, pushNumberOp, Script, sha256d, slpSend, strToBytes, WriterBytes, WriterLength, writeTxOutput, } from 'ecash-lib';
|
|
5
|
+
import { AGORA_LOKAD_ID } from './consts.js';
|
|
6
|
+
/**
|
|
7
|
+
* An Agora offer that can partially be accepted.
|
|
8
|
+
* In contrast to oneshot offers, these can be partially accepted, with the
|
|
9
|
+
* remainder sent back to a new UTXO with the same terms but reduced token
|
|
10
|
+
* amount.
|
|
11
|
+
* This is useful for fungible tokens, where the maker doesn't know upfront how
|
|
12
|
+
* many tokens the takers would like to acquire.
|
|
13
|
+
*
|
|
14
|
+
* The Script enforces that the taker re-creates an offer with the same terms
|
|
15
|
+
* with tokens he didn't buy.
|
|
16
|
+
* It calculates the required sats to accept the offer based on the price per
|
|
17
|
+
* token, and the number of tokens requested by the taker, and enforces the
|
|
18
|
+
* correct amount of satoshis are sent to the P2PKH of the maker of this offer.
|
|
19
|
+
*
|
|
20
|
+
* Offers can also be cancelled by the maker of the offer.
|
|
21
|
+
*
|
|
22
|
+
* One complication is the price calculation, due to eCash's limited precision
|
|
23
|
+
* and range (31-bits plus 1 sign bit) of its Script integers.
|
|
24
|
+
* We employ two strategies to increase precision and range:
|
|
25
|
+
* - "Scaling": We scale up values to the maximum representable, such that we
|
|
26
|
+
* make full use of the 31 bits available. Values that have been scaled up
|
|
27
|
+
* have the prefix "scaled", and the scale factor is "tokenScaleFactor". We
|
|
28
|
+
* only scale token amounts.
|
|
29
|
+
* - "Truncation": We cut off bytes at the "end" of numbers, essentially
|
|
30
|
+
* dividing them by 256 for each truncation, until they fit in 31 bits, so we
|
|
31
|
+
* can use arithmetic opcodes. Later we "un-truncate" values again by adding
|
|
32
|
+
* the bytes back. We use OP_CAT to un-truncate values, which doesn't care
|
|
33
|
+
* about the 31-bit limit. Values that have been truncated have the "trunc"
|
|
34
|
+
* prefix. We truncate both token amounts (by numTokenTruncBytes bytes) and
|
|
35
|
+
* sats amounts (by numSatsTruncBytes).
|
|
36
|
+
*
|
|
37
|
+
* Scaling and truncation can be combined, such that the token price is in
|
|
38
|
+
* "scaledTruncTokensPerTruncSat".
|
|
39
|
+
* Together, they give us a very large range of representable values, while
|
|
40
|
+
* keeping a decent precision.
|
|
41
|
+
*
|
|
42
|
+
* Ideally, eCash can eventually raise the maximum integer size to e.g. 64-bits,
|
|
43
|
+
* which would greatly increase the precision. The strategies employed are
|
|
44
|
+
* useful there too, we simply get a much more accurate price calculation.
|
|
45
|
+
**/
|
|
46
|
+
export class AgoraPartial {
|
|
47
|
+
constructor(params) {
|
|
48
|
+
this.truncTokens = params.truncTokens;
|
|
49
|
+
this.numTokenTruncBytes = params.numTokenTruncBytes;
|
|
50
|
+
this.tokenScaleFactor = params.tokenScaleFactor;
|
|
51
|
+
this.scaledTruncTokensPerTruncSat = params.scaledTruncTokensPerTruncSat;
|
|
52
|
+
this.numSatsTruncBytes = params.numSatsTruncBytes;
|
|
53
|
+
this.makerPk = params.makerPk;
|
|
54
|
+
this.minAcceptedScaledTruncTokens = params.minAcceptedScaledTruncTokens;
|
|
55
|
+
this.tokenId = params.tokenId;
|
|
56
|
+
this.tokenType = params.tokenType;
|
|
57
|
+
this.tokenProtocol = params.tokenProtocol;
|
|
58
|
+
this.scriptLen = params.scriptLen;
|
|
59
|
+
this.dustAmount = params.dustAmount;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Approximate good script parameters for the given offer params.
|
|
63
|
+
* Note: This is not guaranteed to be optimal and is done on a best-effort
|
|
64
|
+
* basis.
|
|
65
|
+
* @param params Offer params to approximate, see AgoraPartialParams for
|
|
66
|
+
* details.
|
|
67
|
+
* @param scriptIntegerBits How many bits Script integers have on the
|
|
68
|
+
* network. On XEC, this must be 32, but if it is raised in the
|
|
69
|
+
* future to e.g. 64-bit integers, this can be set to 64 to greatly
|
|
70
|
+
* increase accuracy.
|
|
71
|
+
**/
|
|
72
|
+
static approximateParams(params, scriptIntegerBits = 32n) {
|
|
73
|
+
if (params.offeredTokens < 1n) {
|
|
74
|
+
throw new Error('offeredTokens must be at least 1');
|
|
75
|
+
}
|
|
76
|
+
if (params.priceNanoSatsPerToken < 1n) {
|
|
77
|
+
throw new Error('priceNanoSatsPerToken must be at least 1');
|
|
78
|
+
}
|
|
79
|
+
if (params.minAcceptedTokens < 1n) {
|
|
80
|
+
throw new Error('minAcceptedTokens must be at least 1');
|
|
81
|
+
}
|
|
82
|
+
if (params.tokenProtocol == 'SLP' &&
|
|
83
|
+
params.offeredTokens > 0xffffffffffffffffn) {
|
|
84
|
+
throw new Error('For SLP, offeredTokens can be at most 0xffffffffffffffff');
|
|
85
|
+
}
|
|
86
|
+
if (params.tokenProtocol == 'ALP' &&
|
|
87
|
+
params.offeredTokens > 0xffffffffffffn) {
|
|
88
|
+
throw new Error('For ALP, offeredTokens can be at most 0xffffffffffff');
|
|
89
|
+
}
|
|
90
|
+
// Script uses 1 bit as sign bit, which we can't use in our calculation
|
|
91
|
+
const scriptIntegerWithoutSignBits = scriptIntegerBits - 1n;
|
|
92
|
+
// Max integer that can be represented in Script on the network
|
|
93
|
+
const maxScriptInt = (1n << scriptIntegerWithoutSignBits) - 1n;
|
|
94
|
+
// Edge case where price can be represented exactly,
|
|
95
|
+
// no need to introduce extra approximation.
|
|
96
|
+
const isPrecisePrice = 1000000000n % params.priceNanoSatsPerToken == 0n;
|
|
97
|
+
// The Script can only handle a maximum level of truncation
|
|
98
|
+
const maxTokenTruncBytes = params.tokenProtocol == 'SLP' ? 5 : 3;
|
|
99
|
+
const minTokenScaleFactor = isPrecisePrice
|
|
100
|
+
? 1n
|
|
101
|
+
: params.minTokenScaleFactor ?? 1000n;
|
|
102
|
+
// If we can't represent the offered tokens in a script int, we truncate 8
|
|
103
|
+
// bits at a time until it fits.
|
|
104
|
+
let truncTokens = params.offeredTokens;
|
|
105
|
+
let numTokenTruncBytes = 0n;
|
|
106
|
+
while (truncTokens * minTokenScaleFactor > maxScriptInt &&
|
|
107
|
+
numTokenTruncBytes < maxTokenTruncBytes) {
|
|
108
|
+
truncTokens >>= 8n;
|
|
109
|
+
numTokenTruncBytes++;
|
|
110
|
+
}
|
|
111
|
+
// Required sats to fully accept the trade (rounded down)
|
|
112
|
+
const requiredSats = (params.offeredTokens * params.priceNanoSatsPerToken) / 1000000000n;
|
|
113
|
+
// For bigger trades (>=2^31 sats), we need also to truncate sats
|
|
114
|
+
let requiredTruncSats = requiredSats;
|
|
115
|
+
let numSatsTruncBytes = 0n;
|
|
116
|
+
while (requiredTruncSats > maxScriptInt) {
|
|
117
|
+
requiredTruncSats >>= 8n;
|
|
118
|
+
numSatsTruncBytes++;
|
|
119
|
+
}
|
|
120
|
+
// We scale up the token values to get some extra precision
|
|
121
|
+
let tokenScaleFactor = maxScriptInt / truncTokens;
|
|
122
|
+
// How many scaled trunc tokens can be gotten for each trunc sat.
|
|
123
|
+
// It is the inverse of the price specified by the user, and truncated +
|
|
124
|
+
// scaled as required by the Script.
|
|
125
|
+
const calcScaledTruncTokensPerTruncSat = () => ((1n << (8n * numSatsTruncBytes)) *
|
|
126
|
+
tokenScaleFactor *
|
|
127
|
+
1000000000n) /
|
|
128
|
+
((1n << (8n * numTokenTruncBytes)) * params.priceNanoSatsPerToken);
|
|
129
|
+
// For trades offering a few tokens for many sats, truncate the sats
|
|
130
|
+
// amounts some more to increase precision.
|
|
131
|
+
const minPriceInteger = params.minPriceInteger ?? 1000n;
|
|
132
|
+
// However, only truncate sats if tokenScaleFactor is well above
|
|
133
|
+
// scaledTruncTokensPerTruncSat, otherwise we lose precision because
|
|
134
|
+
// we're rounding up for the sats calculation in the Script.
|
|
135
|
+
const minScaleRatio = params.minScaleRatio ?? 1000n;
|
|
136
|
+
let scaledTruncTokensPerTruncSat = calcScaledTruncTokensPerTruncSat();
|
|
137
|
+
while (scaledTruncTokensPerTruncSat < minPriceInteger &&
|
|
138
|
+
scaledTruncTokensPerTruncSat * minScaleRatio < tokenScaleFactor) {
|
|
139
|
+
numSatsTruncBytes++;
|
|
140
|
+
scaledTruncTokensPerTruncSat = calcScaledTruncTokensPerTruncSat();
|
|
141
|
+
}
|
|
142
|
+
// Edge case where the sats calculation can go above the integer limit
|
|
143
|
+
if (truncTokens * tokenScaleFactor + scaledTruncTokensPerTruncSat - 1n >
|
|
144
|
+
maxScriptInt) {
|
|
145
|
+
if (truncTokens * tokenScaleFactor <=
|
|
146
|
+
scaledTruncTokensPerTruncSat) {
|
|
147
|
+
// Case where we just overshot the tokenScaleFactor
|
|
148
|
+
tokenScaleFactor /= 2n;
|
|
149
|
+
scaledTruncTokensPerTruncSat =
|
|
150
|
+
calcScaledTruncTokensPerTruncSat();
|
|
151
|
+
}
|
|
152
|
+
const maxTruncTokens = maxScriptInt - scaledTruncTokensPerTruncSat + 1n;
|
|
153
|
+
if (maxTruncTokens < 0n) {
|
|
154
|
+
throw new Error('Parameters cannot be represented in Script');
|
|
155
|
+
}
|
|
156
|
+
if (truncTokens > maxTruncTokens) {
|
|
157
|
+
// Case where truncTokens itself is close to maxScriptInt
|
|
158
|
+
tokenScaleFactor = 1n;
|
|
159
|
+
truncTokens = maxTruncTokens;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Case where scaled tokens would exceed maxScriptInt
|
|
163
|
+
tokenScaleFactor = maxTruncTokens / truncTokens;
|
|
164
|
+
}
|
|
165
|
+
// Recalculate price
|
|
166
|
+
scaledTruncTokensPerTruncSat = calcScaledTruncTokensPerTruncSat();
|
|
167
|
+
}
|
|
168
|
+
// Scale + truncate the minimum accepted tokens
|
|
169
|
+
let minAcceptedScaledTruncTokens = (params.minAcceptedTokens * tokenScaleFactor) >>
|
|
170
|
+
(8n * numTokenTruncBytes);
|
|
171
|
+
const agoraPartial = new AgoraPartial({
|
|
172
|
+
truncTokens,
|
|
173
|
+
numTokenTruncBytes: Number(numTokenTruncBytes),
|
|
174
|
+
tokenScaleFactor,
|
|
175
|
+
scaledTruncTokensPerTruncSat,
|
|
176
|
+
numSatsTruncBytes: Number(numSatsTruncBytes),
|
|
177
|
+
makerPk: params.makerPk,
|
|
178
|
+
minAcceptedScaledTruncTokens,
|
|
179
|
+
tokenId: params.tokenId,
|
|
180
|
+
tokenType: params.tokenType,
|
|
181
|
+
tokenProtocol: params.tokenProtocol,
|
|
182
|
+
scriptLen: 0x7f,
|
|
183
|
+
dustAmount: params.dustAmount ?? DEFAULT_DUST_LIMIT,
|
|
184
|
+
});
|
|
185
|
+
if (agoraPartial.minAcceptedTokens() < 1n) {
|
|
186
|
+
throw new Error('minAcceptedTokens too small, got truncated to 0');
|
|
187
|
+
}
|
|
188
|
+
agoraPartial.updateScriptLen();
|
|
189
|
+
return agoraPartial;
|
|
190
|
+
}
|
|
191
|
+
updateScriptLen() {
|
|
192
|
+
let measuredLength = this.script().cutOutCodesep(0).bytecode.length;
|
|
193
|
+
if (measuredLength >= 0x80) {
|
|
194
|
+
this.scriptLen = 0x80;
|
|
195
|
+
measuredLength = this.script().cutOutCodesep(0).bytecode.length;
|
|
196
|
+
}
|
|
197
|
+
this.scriptLen = measuredLength;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* How many tokens are accually offered by the Script.
|
|
201
|
+
* This may differ from the offeredTokens in the AgoraPartialParams used to
|
|
202
|
+
* approximate this AgoraPartial.
|
|
203
|
+
**/
|
|
204
|
+
offeredTokens() {
|
|
205
|
+
return this.truncTokens << BigInt(8 * this.numTokenTruncBytes);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Actual minimum acceptable tokens of this Script.
|
|
209
|
+
* This may differ from the minAcceptedTokens in the AgoraPartialParams used
|
|
210
|
+
* to approximate this AgoraPartial.
|
|
211
|
+
**/
|
|
212
|
+
minAcceptedTokens() {
|
|
213
|
+
return ((this.minAcceptedScaledTruncTokens <<
|
|
214
|
+
BigInt(8 * this.numTokenTruncBytes)) /
|
|
215
|
+
this.tokenScaleFactor);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Calculate the actually asked satoshi amount for the given accepted number of tokens.
|
|
219
|
+
* This is the exact amount that has to be sent to makerPk's P2PKH address
|
|
220
|
+
* to accept the offer.
|
|
221
|
+
* `acceptedTokens` must have the lowest numTokenTruncBytes bytes set to 0,
|
|
222
|
+
* use prepareAcceptedTokens to do so.
|
|
223
|
+
**/
|
|
224
|
+
askedSats(acceptedTokens) {
|
|
225
|
+
const numSatsTruncBits = BigInt(8 * this.numSatsTruncBytes);
|
|
226
|
+
const numTokenTruncBits = BigInt(8 * this.numTokenTruncBytes);
|
|
227
|
+
const acceptedTruncTokens = acceptedTokens >> numTokenTruncBits;
|
|
228
|
+
if (acceptedTruncTokens << numTokenTruncBits != acceptedTokens) {
|
|
229
|
+
throw new Error(`acceptedTokens must have the last ${numTokenTruncBits} bits ` +
|
|
230
|
+
'set to zero, use prepareAcceptedTokens to get a valid amount');
|
|
231
|
+
}
|
|
232
|
+
// Divide rounding up
|
|
233
|
+
const askedTruncSats = (acceptedTruncTokens * this.tokenScaleFactor +
|
|
234
|
+
this.scaledTruncTokensPerTruncSat -
|
|
235
|
+
1n) /
|
|
236
|
+
this.scaledTruncTokensPerTruncSat;
|
|
237
|
+
// Un-truncate sats
|
|
238
|
+
return askedTruncSats << numSatsTruncBits;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Prepare the given acceptedTokens amount for the Script; `acceptedTokens`
|
|
242
|
+
* must have the lowest numTokenTruncBytes bytes set to 0 and this function
|
|
243
|
+
* does this for us.
|
|
244
|
+
**/
|
|
245
|
+
prepareAcceptedTokens(acceptedTokens) {
|
|
246
|
+
const numTokenTruncBits = BigInt(8 * this.numTokenTruncBytes);
|
|
247
|
+
return (acceptedTokens >> numTokenTruncBits) << numTokenTruncBits;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Calculate the actual priceNanoSatsPerToken of this offer, factoring in
|
|
251
|
+
* all approximation inacurracies.
|
|
252
|
+
* Due to the rounding, the price can change based on the accepted token
|
|
253
|
+
* amount. By default it calculates the price per token for accepting the
|
|
254
|
+
* entire offer.
|
|
255
|
+
**/
|
|
256
|
+
priceNanoSatsPerToken(acceptedTokens) {
|
|
257
|
+
acceptedTokens ?? (acceptedTokens = this.offeredTokens());
|
|
258
|
+
const prepared = this.prepareAcceptedTokens(acceptedTokens);
|
|
259
|
+
const sats = this.askedSats(prepared);
|
|
260
|
+
return (sats * 1000000000n) / prepared;
|
|
261
|
+
}
|
|
262
|
+
adPushdata() {
|
|
263
|
+
const serAdPushdata = (writer) => {
|
|
264
|
+
if (this.tokenProtocol == 'ALP') {
|
|
265
|
+
// On ALP, we signal AGR0 in the pushdata
|
|
266
|
+
writer.putBytes(AGORA_LOKAD_ID);
|
|
267
|
+
writer.putU8(AgoraPartial.COVENANT_VARIANT.length);
|
|
268
|
+
writer.putBytes(strToBytes(AgoraPartial.COVENANT_VARIANT));
|
|
269
|
+
}
|
|
270
|
+
writer.putU8(this.numTokenTruncBytes);
|
|
271
|
+
writer.putU8(this.numSatsTruncBytes);
|
|
272
|
+
writer.putU64(this.tokenScaleFactor);
|
|
273
|
+
writer.putU64(this.scaledTruncTokensPerTruncSat);
|
|
274
|
+
writer.putU64(this.minAcceptedScaledTruncTokens);
|
|
275
|
+
writer.putBytes(this.makerPk);
|
|
276
|
+
};
|
|
277
|
+
const lengthWriter = new WriterLength();
|
|
278
|
+
serAdPushdata(lengthWriter);
|
|
279
|
+
const bytesWriter = new WriterBytes(lengthWriter.length);
|
|
280
|
+
serAdPushdata(bytesWriter);
|
|
281
|
+
return bytesWriter.data;
|
|
282
|
+
}
|
|
283
|
+
covenantConsts() {
|
|
284
|
+
const adPushdata = this.adPushdata();
|
|
285
|
+
// "Consts" is serialized data with the terms of the offer + the token
|
|
286
|
+
// protocol intros.
|
|
287
|
+
if (this.tokenProtocol == 'SLP') {
|
|
288
|
+
const slpSendIntro = slpSend(this.tokenId, this.tokenType, [
|
|
289
|
+
0,
|
|
290
|
+
]).bytecode;
|
|
291
|
+
const covenantConstsWriter = new WriterBytes(slpSendIntro.length + adPushdata.length);
|
|
292
|
+
covenantConstsWriter.putBytes(slpSendIntro);
|
|
293
|
+
covenantConstsWriter.putBytes(adPushdata);
|
|
294
|
+
return [covenantConstsWriter.data, slpSendIntro.length];
|
|
295
|
+
}
|
|
296
|
+
else if (this.tokenProtocol == 'ALP') {
|
|
297
|
+
const alpSendTemplate = alpSend(this.tokenId, this.tokenType, []);
|
|
298
|
+
// ALP SEND section, but without the num amounts
|
|
299
|
+
const alpSendIntro = alpSendTemplate.slice(0, alpSendTemplate.length - 1);
|
|
300
|
+
// eMPP script with Agora ad, but without the ALP section
|
|
301
|
+
const emppIntro = emppScript([adPushdata]);
|
|
302
|
+
const covenantConstsWriter = new WriterBytes(alpSendIntro.length + emppIntro.bytecode.length);
|
|
303
|
+
covenantConstsWriter.putBytes(alpSendIntro);
|
|
304
|
+
covenantConstsWriter.putBytes(emppIntro.bytecode);
|
|
305
|
+
return [covenantConstsWriter.data, alpSendIntro.length];
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
throw new Error('Not implemented');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
script() {
|
|
312
|
+
const [covenantConsts, tokenIntroLen] = this.covenantConsts();
|
|
313
|
+
// Serialize scaled tokens as 8-byte little endian.
|
|
314
|
+
// Even though Script currently doesn't support 64-bit integers,
|
|
315
|
+
// this allows us to eventually upgrade to 64-bit without changing this
|
|
316
|
+
// Script at all.
|
|
317
|
+
const scaledTruncTokens8LeWriter = new WriterBytes(8);
|
|
318
|
+
scaledTruncTokens8LeWriter.putU64(this.truncTokens * this.tokenScaleFactor);
|
|
319
|
+
const scaledTruncTokens8Le = scaledTruncTokens8LeWriter.data;
|
|
320
|
+
return Script.fromOps([
|
|
321
|
+
// # Push consts
|
|
322
|
+
pushBytesOp(covenantConsts),
|
|
323
|
+
// # Push offered token amount as scaled trunc tokens, as u64 LE
|
|
324
|
+
pushBytesOp(scaledTruncTokens8Le),
|
|
325
|
+
// # Use OP_CODESEPERATOR to remove the above two (large) pushops
|
|
326
|
+
// # from the sighash preimage (tx size optimization)
|
|
327
|
+
OP_CODESEPARATOR,
|
|
328
|
+
// OP_ROT(isPurchase, _, _)
|
|
329
|
+
OP_ROT,
|
|
330
|
+
// OP_IF(isPurchase)
|
|
331
|
+
OP_IF,
|
|
332
|
+
// scaledTruncTokens = OP_BIN2NUM(scaledTruncTokens8Le)
|
|
333
|
+
OP_BIN2NUM,
|
|
334
|
+
// OP_ROT(acceptedScaledTruncTokens, _, _)
|
|
335
|
+
OP_ROT,
|
|
336
|
+
// # Verify accepted amount doesn't exceed available amount
|
|
337
|
+
// OP_2DUP(scaledTruncTokens, acceptedScaledTruncTokens)
|
|
338
|
+
OP_2DUP,
|
|
339
|
+
// isNotExcessive = OP_GREATERTHANOREQUAL(scaledTruncTokens,
|
|
340
|
+
// acceptedScaledTruncTokens)
|
|
341
|
+
OP_GREATERTHANOREQUAL,
|
|
342
|
+
// OP_VERIFY(isNotExcessive)
|
|
343
|
+
OP_VERIFY,
|
|
344
|
+
// # Verify accepted amount is above a required minimum
|
|
345
|
+
// OP_DUP(acceptedScaledTruncTokens)
|
|
346
|
+
OP_DUP,
|
|
347
|
+
// # Ensure minimum accepted amount is not violated
|
|
348
|
+
pushNumberOp(this.minAcceptedScaledTruncTokens),
|
|
349
|
+
// isEnough = OP_GREATERTHANOREQUAL(acceptedScaledTruncTokens,
|
|
350
|
+
// minAcceptedScaledTruncTokens)
|
|
351
|
+
OP_GREATERTHANOREQUAL,
|
|
352
|
+
// OP_VERIFY(isEnough)
|
|
353
|
+
OP_VERIFY,
|
|
354
|
+
// # Verify accepted amount is scaled correctly, must be a
|
|
355
|
+
// # multiple of tokenScaleFactor.
|
|
356
|
+
// OP_DUP(acceptedScaledTruncTokens)
|
|
357
|
+
OP_DUP,
|
|
358
|
+
pushNumberOp(this.tokenScaleFactor),
|
|
359
|
+
// scaleRemainder = OP_MOD(acceptedScaledTruncTokens,
|
|
360
|
+
// tokenScaleFactor)
|
|
361
|
+
OP_MOD,
|
|
362
|
+
OP_0,
|
|
363
|
+
// OP_EQUALVERIFY(scaleRemainder, 0)
|
|
364
|
+
OP_EQUALVERIFY,
|
|
365
|
+
// OP_TUCK(_, acceptedScaledTruncTokens);
|
|
366
|
+
OP_TUCK,
|
|
367
|
+
// # Calculate tokens left over after purchase
|
|
368
|
+
// leftoverScaledTruncTokens = OP_SUB(scaledTruncTokens,
|
|
369
|
+
// acceptedScaledTruncTokens)
|
|
370
|
+
OP_SUB,
|
|
371
|
+
// # Get token intro from consts
|
|
372
|
+
// depthConsts = depth_of(consts)
|
|
373
|
+
pushNumberOp(2),
|
|
374
|
+
// consts = OP_PICK(depthConsts);
|
|
375
|
+
OP_PICK,
|
|
376
|
+
// # Size of the token protocol intro
|
|
377
|
+
pushNumberOp(tokenIntroLen),
|
|
378
|
+
// tokenIntro, agoraIntro = OP_SPLIT(consts, introSize)
|
|
379
|
+
OP_SPLIT,
|
|
380
|
+
// OP_DROP(agoraIntro)
|
|
381
|
+
OP_DROP,
|
|
382
|
+
// OP_OVER(leftoverScaledTruncTokens, _)
|
|
383
|
+
OP_OVER,
|
|
384
|
+
// hasLeftover = OP_0NOTEQUAL(leftoverScaledTruncTokens)
|
|
385
|
+
// # (SCRIPT_VERIFY_MINIMALIF is not on eCash, but better be safe)
|
|
386
|
+
OP_0NOTEQUAL,
|
|
387
|
+
// Insert (sub)script that builds the OP_RETURN for SLP/ALP
|
|
388
|
+
...this._scriptBuildOpReturn(tokenIntroLen),
|
|
389
|
+
// # Add trunc padding for sats to un-truncate sats
|
|
390
|
+
pushBytesOp(new Uint8Array(this.numSatsTruncBytes)),
|
|
391
|
+
// outputsOpreturnPad = OP_CAT(opreturnOutput, truncPaddingSats)
|
|
392
|
+
OP_CAT,
|
|
393
|
+
// OP_ROT(acceptedScaledTruncTokens, _, _)
|
|
394
|
+
OP_ROT,
|
|
395
|
+
// # We divide rounding up when we calc sats, so add divisor - 1
|
|
396
|
+
pushNumberOp(this.scaledTruncTokensPerTruncSat - 1n),
|
|
397
|
+
OP_ADD,
|
|
398
|
+
// # Price (scaled + truncated)
|
|
399
|
+
pushNumberOp(this.scaledTruncTokensPerTruncSat),
|
|
400
|
+
// # Calculate how many (truncated) sats the user has to pay
|
|
401
|
+
// requiredTruncSats = OP_DIV(acceptedScaledTruncTokens,
|
|
402
|
+
// scaledTruncTokensPerTruncSat)
|
|
403
|
+
OP_DIV,
|
|
404
|
+
// # Build the required sats with the correct byte length
|
|
405
|
+
// truncLen = 8 - numSatsTruncBytes
|
|
406
|
+
pushNumberOp(8 - this.numSatsTruncBytes),
|
|
407
|
+
// requiredTruncSatsLe = OP_NUM2BIN(requiredTruncSats, truncLen)
|
|
408
|
+
OP_NUM2BIN,
|
|
409
|
+
// # Build OP_RETURN output + satoshi amount (8 bytes LE).
|
|
410
|
+
// # We already added the padding to un-truncate sats in the
|
|
411
|
+
// # previous OP_CAT to the output.
|
|
412
|
+
// outputsOpreturnSats =
|
|
413
|
+
// OP_CAT(outputsOpreturnPad, requiredTruncSatsLe)
|
|
414
|
+
OP_CAT,
|
|
415
|
+
// # Build maker's P2PKH script
|
|
416
|
+
// p2pkhIntro = [25, OP_DUP, OP_HASH160, 20]
|
|
417
|
+
pushBytesOp(new Uint8Array([25, OP_DUP, OP_HASH160, 20])),
|
|
418
|
+
// OP_2OVER(consts, leftoverScaledTruncTokens, _, _);
|
|
419
|
+
OP_2OVER,
|
|
420
|
+
// OP_DROP(leftoverScaledTruncTokens);
|
|
421
|
+
OP_DROP,
|
|
422
|
+
// # Slice out pubkey from the consts (always the last 33 bytes)
|
|
423
|
+
// pubkeyIdx = consts.length - 33
|
|
424
|
+
pushNumberOp(covenantConsts.length - 33),
|
|
425
|
+
// rest, makerPk = OP_SPLIT(consts, pubkeyIdx)
|
|
426
|
+
OP_SPLIT,
|
|
427
|
+
// OP_NIP(rest, _)
|
|
428
|
+
OP_NIP,
|
|
429
|
+
// makerPkh = OP_HASH160(makerPk)
|
|
430
|
+
OP_HASH160,
|
|
431
|
+
// makerP2pkh1 = OP_CAT(p2pkhIntro, makerPkh)
|
|
432
|
+
OP_CAT,
|
|
433
|
+
// p2pkhOutro = [OP_EQUALVERIFY, OP_CHECKSIG]
|
|
434
|
+
pushBytesOp(new Uint8Array([OP_EQUALVERIFY, OP_CHECKSIG])),
|
|
435
|
+
// makerScript = OP_CAT(makerP2pkh1, p2pkhOutro)
|
|
436
|
+
OP_CAT,
|
|
437
|
+
// # Now we have the first 2 outputs: OP_RETURN + maker P2PKH
|
|
438
|
+
// outputsOpreturnMaker = OP_CAT(outputsOpreturnSats, makerScript)
|
|
439
|
+
OP_CAT,
|
|
440
|
+
// # Move to altstack, we need it when calculating hashOutputs
|
|
441
|
+
// OP_TOALTSTACK(outputsOpreturnMaker)
|
|
442
|
+
OP_TOALTSTACK,
|
|
443
|
+
// # Build loopback P2SH, will receive the leftover tokens with
|
|
444
|
+
// # a Script with the same terms.
|
|
445
|
+
// OP_TUCK(_, leftoverScaledTruncTokens);
|
|
446
|
+
OP_TUCK,
|
|
447
|
+
// P2SH has dust sats
|
|
448
|
+
pushNumberOp(this.dustAmount),
|
|
449
|
+
OP_8,
|
|
450
|
+
// dustAmount8le = OP_NUM2BIN(dustAmount, 8)
|
|
451
|
+
OP_NUM2BIN,
|
|
452
|
+
// p2shIntro = [23, OP_HASH160, 20]
|
|
453
|
+
pushBytesOp(new Uint8Array([23, OP_HASH160, 20])),
|
|
454
|
+
// loopbackOutputIntro = OP_CAT(dustAmount8le, p2shIntro);
|
|
455
|
+
OP_CAT,
|
|
456
|
+
// # Build the new redeem script; same terms but different
|
|
457
|
+
// # scaledTruncTokens8Le.
|
|
458
|
+
// # Build opcode to push consts. Sometimes they get long and we
|
|
459
|
+
// # need OP_PUSHDATA1.
|
|
460
|
+
// pushConstsOpcode = if consts.length >= OP_PUSHDATA1 {
|
|
461
|
+
// [OP_PUSHDATA1, consts.length]
|
|
462
|
+
// } else {
|
|
463
|
+
// [consts.length]
|
|
464
|
+
// }
|
|
465
|
+
pushBytesOp(new Uint8Array(covenantConsts.length >= OP_PUSHDATA1
|
|
466
|
+
? [OP_PUSHDATA1, covenantConsts.length]
|
|
467
|
+
: [covenantConsts.length])),
|
|
468
|
+
// OP_2SWAP(consts, leftoverScaledTruncTokens, _, _)
|
|
469
|
+
OP_2SWAP,
|
|
470
|
+
OP_8,
|
|
471
|
+
// OP_TUCK(_, 8)
|
|
472
|
+
OP_TUCK,
|
|
473
|
+
// leftoverScaledTruncTokens8le =
|
|
474
|
+
// OP_NUM2BIN(leftoverScaledTruncTokens, 8)
|
|
475
|
+
OP_NUM2BIN,
|
|
476
|
+
// pushLeftoverScaledTruncTokens8le =
|
|
477
|
+
// OP_CAT(8, leftoverScaledTruncTokens8le)
|
|
478
|
+
OP_CAT,
|
|
479
|
+
// constsPushLeftover =
|
|
480
|
+
// OP_CAT(consts, pushLeftoverScaledTruncTokens8le)
|
|
481
|
+
OP_CAT,
|
|
482
|
+
// # The two ops that push consts plus amount
|
|
483
|
+
// pushState = OP_CAT(pushConstsOpcode, constsPushLeftover)
|
|
484
|
+
OP_CAT,
|
|
485
|
+
// opcodesep = [OP_CODESEPARATOR]
|
|
486
|
+
pushBytesOp(new Uint8Array([OP_CODESEPARATOR])),
|
|
487
|
+
// loopbackScriptIntro = OP_CAT(pushState, opcodesep)
|
|
488
|
+
OP_CAT,
|
|
489
|
+
// depthPreimage4_10 = depth_of(preimage4_10);
|
|
490
|
+
pushNumberOp(3),
|
|
491
|
+
// preimage4_10 = OP_PICK(depthPreimage4_10);
|
|
492
|
+
OP_PICK,
|
|
493
|
+
// scriptCodeIdx = 36 + if scriptLen < 0xfd { 1 } else { 3 }
|
|
494
|
+
pushNumberOp(36 + (this.scriptLen < 0xfd ? 1 : 3)),
|
|
495
|
+
// outpoint, preimage5_10 = OP_SPLIT(preimage4_10, scriptCodeIdx)
|
|
496
|
+
OP_SPLIT,
|
|
497
|
+
// OP_NIP(outpoint, __)
|
|
498
|
+
OP_NIP,
|
|
499
|
+
// # Split out scriptCode
|
|
500
|
+
pushNumberOp(this.scriptLen),
|
|
501
|
+
// script_code, preimage6_10 = OP_SPLIT(preimage5_10, scriptLen)
|
|
502
|
+
OP_SPLIT,
|
|
503
|
+
// # Extract hashOutputs
|
|
504
|
+
OP_12,
|
|
505
|
+
// (preimage6_7, preimage8_10) = OP_SPLIT(preimage6_10, 12)
|
|
506
|
+
OP_SPLIT,
|
|
507
|
+
// OP_NIP(preimage6_7, _)
|
|
508
|
+
OP_NIP,
|
|
509
|
+
// # Split out hashOutputs
|
|
510
|
+
pushNumberOp(32),
|
|
511
|
+
// actualHashOutputs, preimage9_10 = OP_SPLIT(preimage8_10, 32)
|
|
512
|
+
OP_SPLIT,
|
|
513
|
+
// OP_DROP(preimage9_10)
|
|
514
|
+
OP_DROP,
|
|
515
|
+
// # Move to altstack, will be needed later
|
|
516
|
+
// OP_TOALTSTACK(actualHashOutputs)
|
|
517
|
+
OP_TOALTSTACK,
|
|
518
|
+
// # Build redeemScript of loopback P2SH output
|
|
519
|
+
// loopbackScript = OP_CAT(loopbackScriptIntro, scriptCode)
|
|
520
|
+
OP_CAT,
|
|
521
|
+
// # Calculate script hash for P2SH script
|
|
522
|
+
// loopbackScriptHash = OP_HASH160(loopbackScript)
|
|
523
|
+
OP_HASH160,
|
|
524
|
+
// loopbackOutputIntroSh =
|
|
525
|
+
// OP_CAT(loopbackOutputIntro, loopbackScriptHash)
|
|
526
|
+
OP_CAT,
|
|
527
|
+
// p2shEnd = [OP_EQUAL]
|
|
528
|
+
pushBytesOp(new Uint8Array([OP_EQUAL])),
|
|
529
|
+
// # Build loopback P2SH output
|
|
530
|
+
// loopbackOutput = OP_CAT(loopbackOutputIntroSh, p2shEnd)
|
|
531
|
+
OP_CAT,
|
|
532
|
+
// # Check if we have tokens left over and send them back
|
|
533
|
+
// # It is cheaper (in bytes) to build the loopback output and then
|
|
534
|
+
// # throw it away if needed than to not build it at all.
|
|
535
|
+
// OP_SWAP(leftoverScaledTruncTokens, _)
|
|
536
|
+
OP_SWAP,
|
|
537
|
+
// hasLeftover = OP_0NOTEQUAL(leftoverScaledTruncTokens)
|
|
538
|
+
OP_0NOTEQUAL,
|
|
539
|
+
// OP_NOTIF(hasLeftover)
|
|
540
|
+
OP_NOTIF,
|
|
541
|
+
// OP_DROP(loopbackOutput)
|
|
542
|
+
OP_DROP,
|
|
543
|
+
// loopbackOutput = []
|
|
544
|
+
pushBytesOp(new Uint8Array()),
|
|
545
|
+
OP_ENDIF,
|
|
546
|
+
// OP_ROT(buyerOutputs, _, _)
|
|
547
|
+
OP_ROT,
|
|
548
|
+
// # Verify user specified output, otherwise total burn on ALP
|
|
549
|
+
// buyerOutputs, buyerOutputsSize = OP_SIZE(buyerOutputs)
|
|
550
|
+
OP_SIZE,
|
|
551
|
+
// isNotEmpty = OP_0NOTEQUAL(buyerOutputsSize)
|
|
552
|
+
OP_0NOTEQUAL,
|
|
553
|
+
// OP_VERIFY(isNotEmpty)
|
|
554
|
+
OP_VERIFY,
|
|
555
|
+
// # Loopback + taker outputs
|
|
556
|
+
// outputsLoopbackTaker = OP_CAT(loopbackOutput, buyerOutputs)
|
|
557
|
+
OP_CAT,
|
|
558
|
+
// OP_FROMALTSTACK(actualHashOutputs)
|
|
559
|
+
OP_FROMALTSTACK,
|
|
560
|
+
// OP_FROMALTSTACK(outputsOpreturnMaker)
|
|
561
|
+
OP_FROMALTSTACK,
|
|
562
|
+
// OP_ROT(outputsLoopbackTaker, _, _)
|
|
563
|
+
OP_ROT,
|
|
564
|
+
// # Outputs expected by this Script
|
|
565
|
+
// expectedOutputs = OP_CAT(outputsOpreturnMaker,
|
|
566
|
+
// outputsLoopbackTaker)
|
|
567
|
+
OP_CAT,
|
|
568
|
+
// expectedHashOutputs = OP_HASH256(expectedOutputs)
|
|
569
|
+
OP_HASH256,
|
|
570
|
+
// # Verify tx has the expected outputs
|
|
571
|
+
// OP_EQUALVERIFY(actualHashOutputs, expectedHashOutputs)
|
|
572
|
+
OP_EQUALVERIFY,
|
|
573
|
+
// # Build sighash preimage parts 1 to 3 via OP_NUM2BIN
|
|
574
|
+
// txVersion = 2
|
|
575
|
+
OP_2,
|
|
576
|
+
// preimage1_3Len = 4 + 32 + 32
|
|
577
|
+
pushNumberOp(4 + 32 + 32),
|
|
578
|
+
// preimage1_3 = OP_NUM2BIN(txVersion, preimage1_3Len)
|
|
579
|
+
OP_NUM2BIN,
|
|
580
|
+
// # Build full sighash preimage
|
|
581
|
+
// OP_SWAP(preimage4_10, preimage1_3)
|
|
582
|
+
OP_SWAP,
|
|
583
|
+
// preimage = OP_CAT(preimage1_3, preimage4_10)
|
|
584
|
+
OP_CAT,
|
|
585
|
+
// # Sighash for this covenant
|
|
586
|
+
// preimageSha256 = OP_SHA256(preimage)
|
|
587
|
+
OP_SHA256,
|
|
588
|
+
// # Verify our sighash actually matches that of the transaction
|
|
589
|
+
// OP_3DUP(covenantPk, covenantSig, preimageSha256)
|
|
590
|
+
OP_3DUP,
|
|
591
|
+
// OP_ROT(covenantPk, covenantSig, preimageSha256)
|
|
592
|
+
OP_ROT,
|
|
593
|
+
// OP_CHECKDATASIGVERIFY(covenantSig, preimageSha256, covenantPk)
|
|
594
|
+
OP_CHECKDATASIGVERIFY,
|
|
595
|
+
// OP_DROP(preimageSha256)
|
|
596
|
+
OP_DROP,
|
|
597
|
+
// sigHashFlags = [ALL_ANYONECANPAY_BIP143]
|
|
598
|
+
pushBytesOp(new Uint8Array([ALL_ANYONECANPAY_BIP143.toInt()])),
|
|
599
|
+
// covenantSigFlagged = OP_CAT(covenantSig, sigHashFlags)
|
|
600
|
+
OP_CAT,
|
|
601
|
+
// covenantSig, pk = OP_SWAP(covenantPk, covenantSigFlagged)
|
|
602
|
+
OP_SWAP,
|
|
603
|
+
OP_ELSE,
|
|
604
|
+
// # "Cancel" branch, split out the maker pubkey and verify sig
|
|
605
|
+
// # is for the maker pubkey.
|
|
606
|
+
// OP_DROP(scaledTruncTokens8le);
|
|
607
|
+
OP_DROP,
|
|
608
|
+
// pubkeyIdx = consts.length - 33
|
|
609
|
+
pushNumberOp(covenantConsts.length - 33),
|
|
610
|
+
// rest, pk = OP_SPLIT(consts, pubkeyIdx)
|
|
611
|
+
OP_SPLIT,
|
|
612
|
+
// OP_NIP(rest, __)
|
|
613
|
+
OP_NIP,
|
|
614
|
+
OP_ENDIF,
|
|
615
|
+
// # SLP and ALP differ at the end of the Script
|
|
616
|
+
...this._scriptOutro(),
|
|
617
|
+
]);
|
|
618
|
+
}
|
|
619
|
+
_scriptBuildOpReturn(tokenIntroLen) {
|
|
620
|
+
// Script takes in the token amounts and builds the OP_RETURN for the
|
|
621
|
+
// corresponding protocol
|
|
622
|
+
if (this.tokenProtocol == 'SLP') {
|
|
623
|
+
return this._scriptBuildSlpOpReturn();
|
|
624
|
+
}
|
|
625
|
+
else if (this.tokenProtocol == 'ALP') {
|
|
626
|
+
return this._scriptBuildAlpOpReturn(tokenIntroLen);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
throw new Error('Only SLP implemented');
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
_scriptBuildSlpOpReturn() {
|
|
633
|
+
return [
|
|
634
|
+
// # If there's a leftover, append it to the token amounts
|
|
635
|
+
// OP_IF(hasLeftover)
|
|
636
|
+
OP_IF,
|
|
637
|
+
// # Size of an SLP amount
|
|
638
|
+
OP_8,
|
|
639
|
+
// tokenIntro8 = OP_CAT(tokenIntro, 8);
|
|
640
|
+
OP_CAT,
|
|
641
|
+
// OP_OVER(leftoverScaledTruncTokens, _)
|
|
642
|
+
OP_OVER,
|
|
643
|
+
// # Scale down the scaled leftover amount
|
|
644
|
+
pushNumberOp(this.tokenScaleFactor),
|
|
645
|
+
// leftoverTokensTrunc = OP_DIV(leftoverScaledTruncTokens,
|
|
646
|
+
// tokenScaleFactor)
|
|
647
|
+
OP_DIV,
|
|
648
|
+
// # Serialize the leftover trunc tokens (overflow-safe)
|
|
649
|
+
...this._scriptSerTruncTokens(8),
|
|
650
|
+
// # SLP uses big-endian, so we have to use OP_REVERSEBYTES
|
|
651
|
+
// leftoverTokenTruncBe = OP_REVERSEBYTES(leftoverTokenTruncLe)
|
|
652
|
+
OP_REVERSEBYTES,
|
|
653
|
+
// # Bytes to un-truncate the leftover tokens
|
|
654
|
+
pushBytesOp(new Uint8Array(this.numTokenTruncBytes)),
|
|
655
|
+
// # Build the actual 8 byte big-endian leftover
|
|
656
|
+
// leftoverToken8be = OP_CAT(leftoverTokenTruncBe, untruncatePad);
|
|
657
|
+
OP_CAT,
|
|
658
|
+
// # Append the leftover to the token intro
|
|
659
|
+
// tokenScript = OP_CAT(tokenIntro8, leftoverToken8be);
|
|
660
|
+
OP_CAT,
|
|
661
|
+
OP_ENDIF,
|
|
662
|
+
// # Append accepted token amount going to the taker
|
|
663
|
+
// # Size of an SLP amount
|
|
664
|
+
OP_8,
|
|
665
|
+
// tokenScript = OP_CAT(tokenScript, 8)
|
|
666
|
+
OP_CAT,
|
|
667
|
+
// # Get the accepted token amount
|
|
668
|
+
// depthAcceptedScaledTruncTokens =
|
|
669
|
+
// depth_of(acceptedScaledTruncTokens)
|
|
670
|
+
pushNumberOp(2),
|
|
671
|
+
// acceptedScaledTruncTokens =
|
|
672
|
+
// OP_PICK(depthAcceptedScaledTruncTokens)
|
|
673
|
+
OP_PICK,
|
|
674
|
+
// # Scale down the accepted token amount
|
|
675
|
+
pushNumberOp(this.tokenScaleFactor),
|
|
676
|
+
// acceptedTokensTrunc = OP_DIV(acceptedScaledTruncTokens,
|
|
677
|
+
// tokenScaleFactor)
|
|
678
|
+
OP_DIV,
|
|
679
|
+
// # Serialize the accepted token amount (overflow-safe)
|
|
680
|
+
...this._scriptSerTruncTokens(8),
|
|
681
|
+
// # SLP uses big-endian, so we have to use OP_REVERSEBYTES
|
|
682
|
+
// acceptedTokensTruncBe = OP_REVERSEBYTES(acceptedTokensTruncLe);
|
|
683
|
+
OP_REVERSEBYTES,
|
|
684
|
+
// # Bytes to un-truncate the leftover tokens
|
|
685
|
+
pushBytesOp(new Uint8Array(this.numTokenTruncBytes)),
|
|
686
|
+
// acceptedTokens8be = OP_CAT(acceptedTokensTruncBe, untruncatePad);
|
|
687
|
+
OP_CAT,
|
|
688
|
+
// # Finished SLP token script
|
|
689
|
+
// tokenScript = OP_CAT(tokenScript, acceptedTokens8be);
|
|
690
|
+
OP_CAT,
|
|
691
|
+
// # Build OP_RETURN script with 0u64 and size prepended
|
|
692
|
+
// # tokenScript, tokenScriptSize = OP_SIZE(tokenScript)
|
|
693
|
+
OP_SIZE,
|
|
694
|
+
// # Build output value (0u64) + tokenScriptSize.
|
|
695
|
+
// # In case the tokenScriptSize > 127, it will be represented as
|
|
696
|
+
// # 0xXX00 in Script, but it should be just 0xXX.
|
|
697
|
+
// # We could serialize to 2 bytes and then cut one off, but here we
|
|
698
|
+
// # use a neat optimization: We serialize to 9 bytes (resulting in
|
|
699
|
+
// # 0xXX0000000000000000) and then call OP_REVERSEBYTES, which
|
|
700
|
+
// # will result in 0x0000000000000000XX, which is exactly what the
|
|
701
|
+
// # first 9 bytes of the OP_RETURN output should look like.
|
|
702
|
+
OP_9,
|
|
703
|
+
// tokenScriptSize9Le = OP_NUM2BIN(tokenScriptSize, 9)
|
|
704
|
+
OP_NUM2BIN,
|
|
705
|
+
// opreturnValueSize = OP_REVERSEBYTES(tokenScriptSize9Le);
|
|
706
|
+
OP_REVERSEBYTES,
|
|
707
|
+
// OP_SWAP(tokenScript, opreturnValueSize);
|
|
708
|
+
OP_SWAP,
|
|
709
|
+
// opreturnOutput = OP_CAT(opreturnValueSize, tokenScript);
|
|
710
|
+
OP_CAT,
|
|
711
|
+
];
|
|
712
|
+
}
|
|
713
|
+
_scriptBuildAlpOpReturn(tokenIntroLen) {
|
|
714
|
+
// Script takes in the token amounts and builds the OP_RETURN for the
|
|
715
|
+
// ALP token protocol
|
|
716
|
+
return [
|
|
717
|
+
// # If there's a leftover, add it to the token amounts
|
|
718
|
+
// OP_IF(hasLeftover)
|
|
719
|
+
OP_IF,
|
|
720
|
+
// numTokenAmounts = 3
|
|
721
|
+
OP_3,
|
|
722
|
+
// # Append the number of token amounts + the first 0 amount +
|
|
723
|
+
// # un-truncate padding for the 2nd output.
|
|
724
|
+
// # We meld these three ops into one by using OP_NUM2BIN using
|
|
725
|
+
// # 7 + numTokenTruncBytes bytes, which gives us the number of
|
|
726
|
+
// # amounts in the first byte, followed by 6 zero bytes for the
|
|
727
|
+
// # first output, and then numTokenTruncBytes bytes for the
|
|
728
|
+
// # un-truncate padding.
|
|
729
|
+
pushNumberOp(7 + this.numTokenTruncBytes),
|
|
730
|
+
// tokenAmounts1 = OP_NUM2BIN(numTokenAmounts, size)
|
|
731
|
+
OP_NUM2BIN,
|
|
732
|
+
// tokenIntro = OP_CAT(tokenIntro, tokenAmounts1)
|
|
733
|
+
OP_CAT,
|
|
734
|
+
// OP_OVER(leftoverScaledTruncTokens, __)
|
|
735
|
+
OP_OVER,
|
|
736
|
+
// # Scale down the scaled leftover amount
|
|
737
|
+
pushNumberOp(this.tokenScaleFactor),
|
|
738
|
+
// nextSerValue = OP_DIV(leftoverScaledTruncTokens,
|
|
739
|
+
// tokenScaleFactor)
|
|
740
|
+
OP_DIV,
|
|
741
|
+
// # Serialize size for leftoverTokensTrunc, and also already add the un-truncate padding for the 3rd amount
|
|
742
|
+
// # Combining these two ops also doesn't require us to serialize overflow-aware
|
|
743
|
+
pushNumberOp(6 /*- this.numTokenTruncBytes + this.numTokenTruncBytes*/),
|
|
744
|
+
OP_ELSE,
|
|
745
|
+
// # Append the number of token amounts + the first 0 amount +
|
|
746
|
+
// # un-truncate padding for the 3rd output.
|
|
747
|
+
// nextSerValue = 2
|
|
748
|
+
OP_2,
|
|
749
|
+
// serializeSize = 7 + numTokenTruncBytes
|
|
750
|
+
pushNumberOp(7 + this.numTokenTruncBytes),
|
|
751
|
+
OP_ENDIF,
|
|
752
|
+
// tokenAmounts2 = OP_NUM2BIN(numTokenAmounts, serializeSize)
|
|
753
|
+
// # Serialize 1st/2nd output + padding for 2nd/3rd output
|
|
754
|
+
OP_NUM2BIN,
|
|
755
|
+
// # Build the part of the token section that has all the amounts
|
|
756
|
+
// # for the maker (i.e. 0) and covenant loopback, and the
|
|
757
|
+
// # un-truncate padding for the accepted token amount.
|
|
758
|
+
// tokenSection1Pad = OP_CAT(tokenIntro, tokenAmounts2)
|
|
759
|
+
OP_CAT,
|
|
760
|
+
// depthAcceptedScaledTruncTokens =
|
|
761
|
+
// depth_of(acceptedScaledTruncTokens)
|
|
762
|
+
pushNumberOp(2),
|
|
763
|
+
// acceptedScaledTruncTokens =
|
|
764
|
+
// OP_PICK(depthAcceptedScaledTruncTokens)
|
|
765
|
+
OP_PICK,
|
|
766
|
+
// # Scale down the accepted token amount
|
|
767
|
+
pushNumberOp(this.tokenScaleFactor),
|
|
768
|
+
// acceptedTokensTrunc = OP_DIV(acceptedScaledTruncTokens,
|
|
769
|
+
// tokenScaleFactor)
|
|
770
|
+
OP_DIV,
|
|
771
|
+
// # Serialize accepted token amount (overflow-safe)
|
|
772
|
+
...this._scriptSerTruncTokens(6),
|
|
773
|
+
// # Finished token section
|
|
774
|
+
// tokenSection = OP_CAT(tokenSection1Pad, acceptedTokensTruncLe);
|
|
775
|
+
OP_CAT,
|
|
776
|
+
// Turn token section into a pushdata op
|
|
777
|
+
// tokenSection, tokenSectionSize = OP_SIZE(tokenSection)
|
|
778
|
+
OP_SIZE,
|
|
779
|
+
// OP_SWAP(tokenSection, tokenSectionSize)
|
|
780
|
+
OP_SWAP,
|
|
781
|
+
// let pushTokenSection = OP_CAT(tokenSectionSize, tokenSection);
|
|
782
|
+
OP_CAT,
|
|
783
|
+
// Get empp intro from consts
|
|
784
|
+
// depthConsts = depth_of(consts)
|
|
785
|
+
pushNumberOp(3),
|
|
786
|
+
// consts = OP_PICK(depthConsts)
|
|
787
|
+
OP_PICK,
|
|
788
|
+
// # Split out the emppAgoraIntro to prepend it to the OP_RETURN
|
|
789
|
+
pushNumberOp(tokenIntroLen),
|
|
790
|
+
// tokenIntro, emppAgoraIntro = OP_SPLIT(consts, alpIntroSize)
|
|
791
|
+
OP_SPLIT,
|
|
792
|
+
// # We don't need the tokenIntro
|
|
793
|
+
// OP_NIP(tokenIntro, _)
|
|
794
|
+
OP_NIP,
|
|
795
|
+
// Build OP_RETURN script with 0u64 and size prepended
|
|
796
|
+
// OP_SWAP(pushTokenSection, _)
|
|
797
|
+
OP_SWAP,
|
|
798
|
+
// emppScript = OP_CAT(emppIntro, pushTokenSection)
|
|
799
|
+
OP_CAT,
|
|
800
|
+
// emppScript, emppScriptSize = OP_SIZE(emppScript)
|
|
801
|
+
OP_SIZE,
|
|
802
|
+
// # Build output value (0u64) + tokenScriptSize.
|
|
803
|
+
// # See _scriptBuildSlpOpReturn for an explanation
|
|
804
|
+
OP_9,
|
|
805
|
+
// emppScriptSizeZero8 = OP_NUM2BIN(emppScriptSize, _9)
|
|
806
|
+
OP_NUM2BIN,
|
|
807
|
+
// zero8EmppScriptSize = OP_REVERSEBYTES(emppScriptSizeZero8)
|
|
808
|
+
OP_REVERSEBYTES,
|
|
809
|
+
// OP_SWAP(emppScript, zero8EmppScriptSize)
|
|
810
|
+
OP_SWAP,
|
|
811
|
+
// let opreturnOutput = OP_CAT(zero8EmppScriptSize, emppScript)
|
|
812
|
+
OP_CAT,
|
|
813
|
+
];
|
|
814
|
+
}
|
|
815
|
+
_scriptSerTruncTokens(numSerBytes) {
|
|
816
|
+
// Serialize the number on the stack using the configured truncation
|
|
817
|
+
if (this.numTokenTruncBytes == numSerBytes - 3) {
|
|
818
|
+
// Edge case where we only have 3 bytes space to serialize the
|
|
819
|
+
// number, but if the MSB of the number is set, OP_NUM2BIN will
|
|
820
|
+
// serialize using 4 bytes (with the last byte being just 0x00),
|
|
821
|
+
// so we always serialize using 4 bytes and then cut the last
|
|
822
|
+
// byte (that's always 0x00) off.
|
|
823
|
+
return [
|
|
824
|
+
pushNumberOp(4),
|
|
825
|
+
OP_NUM2BIN,
|
|
826
|
+
pushNumberOp(3),
|
|
827
|
+
OP_SPLIT,
|
|
828
|
+
OP_DROP,
|
|
829
|
+
];
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
// If we have 4 or more bytes space, we can always serialize
|
|
833
|
+
// just using normal OP_NUM2BIN.
|
|
834
|
+
return [
|
|
835
|
+
pushNumberOp(numSerBytes - this.numTokenTruncBytes),
|
|
836
|
+
OP_NUM2BIN,
|
|
837
|
+
];
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
_scriptOutro() {
|
|
841
|
+
if (this.tokenProtocol == 'SLP') {
|
|
842
|
+
// Verify the sig, and also ensure the first two push ops of the
|
|
843
|
+
// scriptSig are "AGR0" "PARTIAL", which will always have to be the
|
|
844
|
+
// first two ops because of the cleanstack rule.
|
|
845
|
+
return [
|
|
846
|
+
OP_CHECKSIGVERIFY,
|
|
847
|
+
pushBytesOp(strToBytes(AgoraPartial.COVENANT_VARIANT)),
|
|
848
|
+
OP_EQUALVERIFY,
|
|
849
|
+
pushBytesOp(AGORA_LOKAD_ID),
|
|
850
|
+
OP_EQUAL,
|
|
851
|
+
];
|
|
852
|
+
}
|
|
853
|
+
else if (this.tokenProtocol == 'ALP') {
|
|
854
|
+
return [OP_CHECKSIG];
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
throw new Error('Only SLP implemented');
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* redeemScript of the Script advertizing this offer.
|
|
862
|
+
* It requires a setup tx followed by the actual offer, which reveals
|
|
863
|
+
* the covenantConsts.
|
|
864
|
+
* The reason we have an OP_CHECKSIGVERIFY (as opposed to just leaving it
|
|
865
|
+
* as "anyone can spend with this pushdata") is so that others on the
|
|
866
|
+
* network can't spend this UTXO (and potentially take the tokens in it),
|
|
867
|
+
* and only the maker can spend it.
|
|
868
|
+
**/
|
|
869
|
+
adScript() {
|
|
870
|
+
const [covenantConsts, _] = this.covenantConsts();
|
|
871
|
+
return Script.fromOps([
|
|
872
|
+
pushBytesOp(covenantConsts),
|
|
873
|
+
pushNumberOp(covenantConsts.length - 33),
|
|
874
|
+
OP_SPLIT,
|
|
875
|
+
OP_NIP,
|
|
876
|
+
OP_CHECKSIGVERIFY,
|
|
877
|
+
pushBytesOp(strToBytes(AgoraPartial.COVENANT_VARIANT)),
|
|
878
|
+
OP_EQUALVERIFY,
|
|
879
|
+
pushBytesOp(AGORA_LOKAD_ID),
|
|
880
|
+
OP_EQUAL,
|
|
881
|
+
]);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
AgoraPartial.COVENANT_VARIANT = 'PARTIAL';
|
|
885
|
+
function makeScriptSigIntro(tokenProtocol) {
|
|
886
|
+
switch (tokenProtocol) {
|
|
887
|
+
case 'SLP':
|
|
888
|
+
// For SLP, we need to add "AGR0" "PARTIAL" at the beginning of the
|
|
889
|
+
// scriptSig, to advertize it via the LOKAD ID. ALP uses the
|
|
890
|
+
// OP_RETURN, so there this is not needed.
|
|
891
|
+
return [
|
|
892
|
+
pushBytesOp(AGORA_LOKAD_ID),
|
|
893
|
+
pushBytesOp(strToBytes(AgoraPartial.COVENANT_VARIANT)),
|
|
894
|
+
];
|
|
895
|
+
default:
|
|
896
|
+
return [];
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
export const AgoraPartialSignatory = (params, acceptedTruncTokens, covenantSk, covenantPk) => {
|
|
900
|
+
return (ecc, input) => {
|
|
901
|
+
const preimage = input.sigHashPreimage(ALL_ANYONECANPAY_BIP143, 0);
|
|
902
|
+
const sighash = sha256d(preimage.bytes);
|
|
903
|
+
const covenantSig = ecc.schnorrSign(covenantSk, sighash);
|
|
904
|
+
const hasLeftover = params.truncTokens > acceptedTruncTokens;
|
|
905
|
+
const buyerOutputIdx = hasLeftover ? 3 : 2;
|
|
906
|
+
const buyerOutputs = input.unsignedTx.tx.outputs.slice(buyerOutputIdx);
|
|
907
|
+
const serTakerOutputs = (writer) => {
|
|
908
|
+
for (const output of buyerOutputs) {
|
|
909
|
+
writeTxOutput(output, writer);
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
const writerLength = new WriterLength();
|
|
913
|
+
serTakerOutputs(writerLength);
|
|
914
|
+
const writer = new WriterBytes(writerLength.length);
|
|
915
|
+
serTakerOutputs(writer);
|
|
916
|
+
const buyerOutputsSer = writer.data;
|
|
917
|
+
return Script.fromOps([
|
|
918
|
+
...makeScriptSigIntro(params.tokenProtocol),
|
|
919
|
+
pushBytesOp(covenantPk),
|
|
920
|
+
pushBytesOp(covenantSig),
|
|
921
|
+
pushBytesOp(buyerOutputsSer),
|
|
922
|
+
pushBytesOp(preimage.bytes.slice(4 + 32 + 32)), // preimage_4_10
|
|
923
|
+
pushNumberOp(acceptedTruncTokens * params.tokenScaleFactor),
|
|
924
|
+
OP_1, // is_purchase = true
|
|
925
|
+
pushBytesOp(preimage.redeemScript.bytecode),
|
|
926
|
+
]);
|
|
927
|
+
};
|
|
928
|
+
};
|
|
929
|
+
export const AgoraPartialCancelSignatory = (makerSk, tokenProtocol) => {
|
|
930
|
+
return (ecc, input) => {
|
|
931
|
+
const preimage = input.sigHashPreimage(ALL_BIP143, 0);
|
|
932
|
+
const sighash = sha256d(preimage.bytes);
|
|
933
|
+
const cancelSig = flagSignature(ecc.schnorrSign(makerSk, sighash), ALL_BIP143);
|
|
934
|
+
return Script.fromOps([
|
|
935
|
+
...makeScriptSigIntro(tokenProtocol),
|
|
936
|
+
pushBytesOp(cancelSig),
|
|
937
|
+
OP_0, // is_purchase = false
|
|
938
|
+
pushBytesOp(preimage.redeemScript.bytecode),
|
|
939
|
+
]);
|
|
940
|
+
};
|
|
941
|
+
};
|
|
942
|
+
export const AgoraPartialAdSignatory = (makerSk) => {
|
|
943
|
+
return (ecc, input) => {
|
|
944
|
+
const preimage = input.sigHashPreimage(ALL_BIP143);
|
|
945
|
+
const sighash = sha256d(preimage.bytes);
|
|
946
|
+
const makerSig = flagSignature(ecc.schnorrSign(makerSk, sighash), ALL_BIP143);
|
|
947
|
+
return Script.fromOps([
|
|
948
|
+
pushBytesOp(AGORA_LOKAD_ID),
|
|
949
|
+
pushBytesOp(strToBytes(AgoraPartial.COVENANT_VARIANT)),
|
|
950
|
+
pushBytesOp(makerSig),
|
|
951
|
+
pushBytesOp(preimage.redeemScript.bytecode),
|
|
952
|
+
]);
|
|
953
|
+
};
|
|
954
|
+
};
|
|
955
|
+
//# sourceMappingURL=partial.js.map
|