@buildonspark/spark-sdk 0.1.45 → 0.1.47
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/CHANGELOG.md +22 -0
- package/dist/{chunk-I54FARY2.js → chunk-EAP3U3CW.js} +14 -14
- package/dist/chunk-GWFQ7EBA.js +3773 -0
- package/dist/{chunk-J2IE4Z7Y.js → chunk-NNX4OK44.js} +3487 -934
- package/dist/{RequestLightningSendInput-Du0z7Om7.d.cts → client-CvpTRpcw.d.cts} +422 -212
- package/dist/{RequestLightningSendInput-DEPd_fPO.d.ts → client-D7KgLN44.d.ts} +422 -212
- package/dist/graphql/objects/index.d.cts +5 -9
- package/dist/graphql/objects/index.d.ts +5 -9
- package/dist/graphql/objects/index.js +1 -1
- package/dist/index.cjs +20461 -23377
- package/dist/index.d.cts +15 -769
- package/dist/index.d.ts +15 -769
- package/dist/index.js +81 -71
- package/dist/index.node.cjs +21994 -25018
- package/dist/index.node.d.cts +312 -34
- package/dist/index.node.d.ts +312 -34
- package/dist/index.node.js +82 -176
- package/dist/native/index.cjs +22847 -25841
- package/dist/native/index.d.cts +974 -1138
- package/dist/native/index.d.ts +974 -1138
- package/dist/native/index.js +10604 -13592
- package/dist/proto/lrc20.d.cts +2 -2
- package/dist/proto/lrc20.d.ts +2 -2
- package/dist/proto/lrc20.js +3098 -46
- package/dist/proto/spark.d.cts +1 -1
- package/dist/proto/spark.d.ts +1 -1
- package/dist/proto/spark_token.d.cts +1 -1
- package/dist/proto/spark_token.d.ts +1 -1
- package/dist/{sdk-types-Cc4l4kb1.d.ts → sdk-types-BGCeea0G.d.ts} +1 -1
- package/dist/{sdk-types-B0SwjolI.d.cts → sdk-types-XUeQMLFP.d.cts} +1 -1
- package/dist/{spark-dM7EYXYQ.d.cts → spark-BbUrbvZz.d.cts} +1 -1
- package/dist/{spark-dM7EYXYQ.d.ts → spark-BbUrbvZz.d.ts} +1 -1
- package/dist/spark-wallet-BAFPpPtY.d.cts +923 -0
- package/dist/spark-wallet-CJkQW8pK.d.ts +923 -0
- package/dist/spark_bindings/native/index.d.cts +1 -1
- package/dist/spark_bindings/native/index.d.ts +1 -1
- package/dist/spark_bindings/wasm/index.d.cts +1 -1
- package/dist/spark_bindings/wasm/index.d.ts +1 -1
- package/dist/{services/index.cjs → tests/test-utils.cjs} +2512 -4380
- package/dist/tests/test-utils.d.cts +79 -0
- package/dist/tests/test-utils.d.ts +79 -0
- package/dist/tests/test-utils.js +85 -0
- package/dist/types/index.d.cts +5 -9
- package/dist/types/index.d.ts +5 -9
- package/dist/types/index.js +5 -5
- package/dist/{types-C-Rp0Oo7.d.cts → types-BADxR3bm.d.cts} +1 -1
- package/dist/{types-C-Rp0Oo7.d.ts → types-BADxR3bm.d.ts} +1 -1
- package/package.json +7 -35
- package/src/graphql/client.ts +59 -20
- package/src/index.node.ts +28 -2
- package/src/index.ts +31 -1
- package/src/native/index.ts +16 -2
- package/src/services/config.ts +4 -6
- package/src/services/connection.ts +131 -64
- package/src/services/lightning.ts +1 -2
- package/src/services/token-transactions.ts +7 -7
- package/src/services/transfer.ts +1 -1
- package/src/services/tree-creation.ts +1 -1
- package/src/services/wallet-config.ts +18 -10
- package/src/signer/signer.react-native.ts +2 -5
- package/src/signer/signer.ts +138 -64
- package/src/signer/types.ts +52 -0
- package/src/spark-wallet/spark-wallet.ts +79 -36
- package/src/spark-wallet/types.ts +4 -4
- package/src/tests/integration/coop-exit.test.ts +2 -1
- package/src/tests/integration/lightning.test.ts +2 -2
- package/src/tests/integration/swap.test.ts +1 -1
- package/src/tests/integration/transfer.test.ts +5 -5
- package/src/tests/integration/tree-creation.test.ts +1 -1
- package/src/tests/integration/wallet.test.ts +1 -0
- package/src/tests/isHermeticTest.ts +3 -24
- package/src/tests/{test-util.ts → test-utils.ts} +3 -7
- package/src/tests/wrapWithOtelSpan.test.ts +1 -1
- package/src/{address → utils}/address.ts +1 -1
- package/src/utils/crypto.ts +19 -9
- package/src/utils/index.ts +2 -0
- package/src/utils/network.ts +17 -0
- package/src/utils/secret-sharing.ts +1 -2
- package/src/utils/signing.ts +1 -1
- package/src/utils/token-transactions.ts +3 -3
- package/src/utils/unilateral-exit.ts +32 -0
- package/src/utils/xchain-address.ts +1 -1
- package/dist/BitcoinNetwork-TnABML0T.d.cts +0 -18
- package/dist/BitcoinNetwork-TnABML0T.d.ts +0 -18
- package/dist/LightningSendFeeEstimateInput-BgOhEAI-.d.cts +0 -10
- package/dist/LightningSendFeeEstimateInput-BgOhEAI-.d.ts +0 -10
- package/dist/address/index.cjs +0 -458
- package/dist/address/index.d.cts +0 -32
- package/dist/address/index.d.ts +0 -32
- package/dist/address/index.js +0 -17
- package/dist/chunk-5FUB65LX.js +0 -838
- package/dist/chunk-6264CGDM.js +0 -113
- package/dist/chunk-7V6N75CC.js +0 -24
- package/dist/chunk-C2S227QR.js +0 -2336
- package/dist/chunk-GSI4OLXZ.js +0 -117
- package/dist/chunk-GZ5IPPJ2.js +0 -170
- package/dist/chunk-HWJWKEIU.js +0 -75
- package/dist/chunk-KMUMFYFX.js +0 -137
- package/dist/chunk-L3EHBOUX.js +0 -0
- package/dist/chunk-NSJF5F5O.js +0 -325
- package/dist/chunk-NTFKFRQ2.js +0 -3146
- package/dist/chunk-PQN3C2MF.js +0 -1122
- package/dist/chunk-QNNSEJ4P.js +0 -232
- package/dist/chunk-R5PXJZQS.js +0 -277
- package/dist/chunk-VTUGIIWI.js +0 -0
- package/dist/chunk-YUPMXTCJ.js +0 -622
- package/dist/chunk-Z5HIAYFT.js +0 -84
- package/dist/index-B2AwKW5J.d.cts +0 -214
- package/dist/index-CJDi1HWc.d.ts +0 -214
- package/dist/network-BTJl-Sul.d.ts +0 -46
- package/dist/network-CqgsdUF2.d.cts +0 -46
- package/dist/services/config.cjs +0 -2354
- package/dist/services/config.d.cts +0 -42
- package/dist/services/config.d.ts +0 -42
- package/dist/services/config.js +0 -17
- package/dist/services/connection.cjs +0 -17691
- package/dist/services/connection.d.cts +0 -95
- package/dist/services/connection.d.ts +0 -95
- package/dist/services/connection.js +0 -11
- package/dist/services/index.d.cts +0 -21
- package/dist/services/index.d.ts +0 -21
- package/dist/services/index.js +0 -58
- package/dist/services/lrc-connection.cjs +0 -4713
- package/dist/services/lrc-connection.d.cts +0 -34
- package/dist/services/lrc-connection.d.ts +0 -34
- package/dist/services/lrc-connection.js +0 -11
- package/dist/services/token-transactions.cjs +0 -2877
- package/dist/services/token-transactions.d.cts +0 -75
- package/dist/services/token-transactions.d.ts +0 -75
- package/dist/services/token-transactions.js +0 -15
- package/dist/services/wallet-config.cjs +0 -340
- package/dist/services/wallet-config.d.cts +0 -56
- package/dist/services/wallet-config.d.ts +0 -56
- package/dist/services/wallet-config.js +0 -33
- package/dist/signer/signer.cjs +0 -2004
- package/dist/signer/signer.d.cts +0 -10
- package/dist/signer/signer.d.ts +0 -10
- package/dist/signer/signer.js +0 -24
- package/dist/signer-BocS_J6B.d.ts +0 -187
- package/dist/signer-DKS0AJkw.d.cts +0 -187
- package/dist/utils/index.cjs +0 -2947
- package/dist/utils/index.d.cts +0 -18
- package/dist/utils/index.d.ts +0 -18
- package/dist/utils/index.js +0 -157
- package/src/address/index.ts +0 -1
- package/src/services/lrc-connection.ts +0 -215
package/dist/chunk-C2S227QR.js
DELETED
|
@@ -1,2336 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
calculateAvailableTokenAmount,
|
|
3
|
-
checkIfSelectedOutputsAreAvailable,
|
|
4
|
-
collectResponses
|
|
5
|
-
} from "./chunk-HWJWKEIU.js";
|
|
6
|
-
import {
|
|
7
|
-
decodeSparkAddress
|
|
8
|
-
} from "./chunk-KMUMFYFX.js";
|
|
9
|
-
import {
|
|
10
|
-
bigIntToPrivateKey,
|
|
11
|
-
recoverSecret
|
|
12
|
-
} from "./chunk-QNNSEJ4P.js";
|
|
13
|
-
import {
|
|
14
|
-
InternalValidationError,
|
|
15
|
-
NetworkError,
|
|
16
|
-
ValidationError
|
|
17
|
-
} from "./chunk-GSI4OLXZ.js";
|
|
18
|
-
import {
|
|
19
|
-
Buffer
|
|
20
|
-
} from "./chunk-MVRQ5US7.js";
|
|
21
|
-
|
|
22
|
-
// src/services/token-transactions.ts
|
|
23
|
-
import {
|
|
24
|
-
bytesToHex,
|
|
25
|
-
bytesToNumberBE,
|
|
26
|
-
numberToBytesBE
|
|
27
|
-
} from "@noble/curves/abstract/utils";
|
|
28
|
-
import { secp256k1 as secp256k12 } from "@noble/curves/secp256k1";
|
|
29
|
-
|
|
30
|
-
// src/utils/token-hashing.ts
|
|
31
|
-
import { sha256 } from "@noble/hashes/sha2";
|
|
32
|
-
function hashTokenTransaction(tokenTransaction, partialHash = false) {
|
|
33
|
-
switch (tokenTransaction.version) {
|
|
34
|
-
case 0:
|
|
35
|
-
return hashTokenTransactionV0(tokenTransaction, partialHash);
|
|
36
|
-
case 1:
|
|
37
|
-
return hashTokenTransactionV1(tokenTransaction, partialHash);
|
|
38
|
-
default:
|
|
39
|
-
throw new ValidationError("invalid token transaction version", {
|
|
40
|
-
field: "tokenTransaction.version",
|
|
41
|
-
value: tokenTransaction.version
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function hashTokenTransactionV0(tokenTransaction, partialHash = false) {
|
|
46
|
-
if (!tokenTransaction) {
|
|
47
|
-
throw new ValidationError("token transaction cannot be nil", {
|
|
48
|
-
field: "tokenTransaction"
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
let allHashes = [];
|
|
52
|
-
if (tokenTransaction.tokenInputs?.$case === "transferInput") {
|
|
53
|
-
if (!tokenTransaction.tokenInputs.transferInput.outputsToSpend) {
|
|
54
|
-
throw new ValidationError("outputs to spend cannot be null", {
|
|
55
|
-
field: "tokenInputs.transferInput.outputsToSpend"
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
if (tokenTransaction.tokenInputs.transferInput.outputsToSpend.length === 0) {
|
|
59
|
-
throw new ValidationError("outputs to spend cannot be empty", {
|
|
60
|
-
field: "tokenInputs.transferInput.outputsToSpend"
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
for (const [
|
|
64
|
-
i,
|
|
65
|
-
output
|
|
66
|
-
] of tokenTransaction.tokenInputs.transferInput.outputsToSpend.entries()) {
|
|
67
|
-
if (!output) {
|
|
68
|
-
throw new ValidationError(`output cannot be null at index ${i}`, {
|
|
69
|
-
field: `tokenInputs.transferInput.outputsToSpend[${i}]`,
|
|
70
|
-
index: i
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
const hashObj2 = sha256.create();
|
|
74
|
-
if (output.prevTokenTransactionHash) {
|
|
75
|
-
const prevHash = output.prevTokenTransactionHash;
|
|
76
|
-
if (output.prevTokenTransactionHash.length !== 32) {
|
|
77
|
-
throw new ValidationError(
|
|
78
|
-
`invalid previous transaction hash length at index ${i}`,
|
|
79
|
-
{
|
|
80
|
-
field: `tokenInputs.transferInput.outputsToSpend[${i}].prevTokenTransactionHash`,
|
|
81
|
-
value: prevHash,
|
|
82
|
-
expectedLength: 32,
|
|
83
|
-
actualLength: prevHash.length,
|
|
84
|
-
index: i
|
|
85
|
-
}
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
hashObj2.update(output.prevTokenTransactionHash);
|
|
89
|
-
}
|
|
90
|
-
const voutBytes = new Uint8Array(4);
|
|
91
|
-
new DataView(voutBytes.buffer).setUint32(
|
|
92
|
-
0,
|
|
93
|
-
output.prevTokenTransactionVout,
|
|
94
|
-
false
|
|
95
|
-
);
|
|
96
|
-
hashObj2.update(voutBytes);
|
|
97
|
-
allHashes.push(hashObj2.digest());
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
if (tokenTransaction.tokenInputs?.$case === "mintInput") {
|
|
101
|
-
const hashObj2 = sha256.create();
|
|
102
|
-
if (tokenTransaction.tokenInputs.mintInput.issuerPublicKey) {
|
|
103
|
-
const issuerPubKey = tokenTransaction.tokenInputs.mintInput.issuerPublicKey;
|
|
104
|
-
if (issuerPubKey.length === 0) {
|
|
105
|
-
throw new ValidationError("issuer public key cannot be empty", {
|
|
106
|
-
field: "tokenInputs.mintInput.issuerPublicKey",
|
|
107
|
-
value: issuerPubKey,
|
|
108
|
-
expectedLength: 1,
|
|
109
|
-
actualLength: 0
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
hashObj2.update(issuerPubKey);
|
|
113
|
-
let timestampValue = 0;
|
|
114
|
-
const mintInput = tokenTransaction.tokenInputs.mintInput;
|
|
115
|
-
if ("issuerProvidedTimestamp" in mintInput) {
|
|
116
|
-
const v0MintInput = mintInput;
|
|
117
|
-
if (v0MintInput.issuerProvidedTimestamp != 0) {
|
|
118
|
-
timestampValue = v0MintInput.issuerProvidedTimestamp;
|
|
119
|
-
}
|
|
120
|
-
} else if ("clientCreatedTimestamp" in tokenTransaction && tokenTransaction.clientCreatedTimestamp) {
|
|
121
|
-
timestampValue = tokenTransaction.clientCreatedTimestamp.getTime();
|
|
122
|
-
}
|
|
123
|
-
if (timestampValue != 0) {
|
|
124
|
-
const timestampBytes = new Uint8Array(8);
|
|
125
|
-
new DataView(timestampBytes.buffer).setBigUint64(
|
|
126
|
-
0,
|
|
127
|
-
BigInt(timestampValue),
|
|
128
|
-
true
|
|
129
|
-
// true for little-endian to match Go implementation
|
|
130
|
-
);
|
|
131
|
-
hashObj2.update(timestampBytes);
|
|
132
|
-
}
|
|
133
|
-
allHashes.push(hashObj2.digest());
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (tokenTransaction.tokenInputs?.$case === "createInput") {
|
|
137
|
-
const issuerPubKeyHashObj = sha256.create();
|
|
138
|
-
const createInput = tokenTransaction.tokenInputs.createInput;
|
|
139
|
-
if (!createInput.issuerPublicKey || createInput.issuerPublicKey.length === 0) {
|
|
140
|
-
throw new ValidationError("issuer public key cannot be nil or empty", {
|
|
141
|
-
field: "tokenInputs.createInput.issuerPublicKey"
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
issuerPubKeyHashObj.update(createInput.issuerPublicKey);
|
|
145
|
-
allHashes.push(issuerPubKeyHashObj.digest());
|
|
146
|
-
const tokenNameHashObj = sha256.create();
|
|
147
|
-
if (!createInput.tokenName || createInput.tokenName.length === 0) {
|
|
148
|
-
throw new ValidationError("token name cannot be empty", {
|
|
149
|
-
field: "tokenInputs.createInput.tokenName"
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
if (createInput.tokenName.length > 20) {
|
|
153
|
-
throw new ValidationError("token name cannot be longer than 20 bytes", {
|
|
154
|
-
field: "tokenInputs.createInput.tokenName",
|
|
155
|
-
value: createInput.tokenName,
|
|
156
|
-
expectedLength: 20,
|
|
157
|
-
actualLength: createInput.tokenName.length
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
const tokenNameBytes = new Uint8Array(20);
|
|
161
|
-
const tokenNameEncoder = new TextEncoder();
|
|
162
|
-
tokenNameBytes.set(tokenNameEncoder.encode(createInput.tokenName));
|
|
163
|
-
tokenNameHashObj.update(tokenNameBytes);
|
|
164
|
-
allHashes.push(tokenNameHashObj.digest());
|
|
165
|
-
const tokenTickerHashObj = sha256.create();
|
|
166
|
-
if (!createInput.tokenTicker || createInput.tokenTicker.length === 0) {
|
|
167
|
-
throw new ValidationError("token ticker cannot be empty", {
|
|
168
|
-
field: "tokenInputs.createInput.tokenTicker"
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
if (createInput.tokenTicker.length > 6) {
|
|
172
|
-
throw new ValidationError("token ticker cannot be longer than 6 bytes", {
|
|
173
|
-
field: "tokenInputs.createInput.tokenTicker",
|
|
174
|
-
value: createInput.tokenTicker,
|
|
175
|
-
expectedLength: 6,
|
|
176
|
-
actualLength: createInput.tokenTicker.length
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
const tokenTickerBytes = new Uint8Array(6);
|
|
180
|
-
const tokenTickerEncoder = new TextEncoder();
|
|
181
|
-
tokenTickerBytes.set(tokenTickerEncoder.encode(createInput.tokenTicker));
|
|
182
|
-
tokenTickerHashObj.update(tokenTickerBytes);
|
|
183
|
-
allHashes.push(tokenTickerHashObj.digest());
|
|
184
|
-
const decimalsHashObj = sha256.create();
|
|
185
|
-
const decimalsBytes = new Uint8Array(4);
|
|
186
|
-
new DataView(decimalsBytes.buffer).setUint32(
|
|
187
|
-
0,
|
|
188
|
-
createInput.decimals,
|
|
189
|
-
false
|
|
190
|
-
);
|
|
191
|
-
decimalsHashObj.update(decimalsBytes);
|
|
192
|
-
allHashes.push(decimalsHashObj.digest());
|
|
193
|
-
const maxSupplyHashObj = sha256.create();
|
|
194
|
-
if (!createInput.maxSupply) {
|
|
195
|
-
throw new ValidationError("max supply cannot be nil", {
|
|
196
|
-
field: "tokenInputs.createInput.maxSupply"
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
if (createInput.maxSupply.length !== 16) {
|
|
200
|
-
throw new ValidationError("max supply must be exactly 16 bytes", {
|
|
201
|
-
field: "tokenInputs.createInput.maxSupply",
|
|
202
|
-
value: createInput.maxSupply,
|
|
203
|
-
expectedLength: 16,
|
|
204
|
-
actualLength: createInput.maxSupply.length
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
maxSupplyHashObj.update(createInput.maxSupply);
|
|
208
|
-
allHashes.push(maxSupplyHashObj.digest());
|
|
209
|
-
const isFreezableHashObj = sha256.create();
|
|
210
|
-
const isFreezableByte = new Uint8Array([createInput.isFreezable ? 1 : 0]);
|
|
211
|
-
isFreezableHashObj.update(isFreezableByte);
|
|
212
|
-
allHashes.push(isFreezableHashObj.digest());
|
|
213
|
-
const creationEntityHashObj = sha256.create();
|
|
214
|
-
if (!partialHash && createInput.creationEntityPublicKey) {
|
|
215
|
-
creationEntityHashObj.update(createInput.creationEntityPublicKey);
|
|
216
|
-
}
|
|
217
|
-
allHashes.push(creationEntityHashObj.digest());
|
|
218
|
-
}
|
|
219
|
-
if (!tokenTransaction.tokenOutputs) {
|
|
220
|
-
throw new ValidationError("token outputs cannot be null", {
|
|
221
|
-
field: "tokenOutputs"
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
if (tokenTransaction.tokenOutputs.length === 0 && tokenTransaction.tokenInputs?.$case !== "createInput") {
|
|
225
|
-
throw new ValidationError("token outputs cannot be empty", {
|
|
226
|
-
field: "tokenOutputs"
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
for (const [i, output] of tokenTransaction.tokenOutputs.entries()) {
|
|
230
|
-
if (!output) {
|
|
231
|
-
throw new ValidationError(`output cannot be null at index ${i}`, {
|
|
232
|
-
field: `tokenOutputs[${i}]`,
|
|
233
|
-
index: i
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
const hashObj2 = sha256.create();
|
|
237
|
-
if (output.id && !partialHash) {
|
|
238
|
-
if (output.id.length === 0) {
|
|
239
|
-
throw new ValidationError(`output ID at index ${i} cannot be empty`, {
|
|
240
|
-
field: `tokenOutputs[${i}].id`,
|
|
241
|
-
index: i
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
hashObj2.update(new TextEncoder().encode(output.id));
|
|
245
|
-
}
|
|
246
|
-
if (output.ownerPublicKey) {
|
|
247
|
-
if (output.ownerPublicKey.length === 0) {
|
|
248
|
-
throw new ValidationError(
|
|
249
|
-
`owner public key at index ${i} cannot be empty`,
|
|
250
|
-
{
|
|
251
|
-
field: `tokenOutputs[${i}].ownerPublicKey`,
|
|
252
|
-
index: i
|
|
253
|
-
}
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
hashObj2.update(output.ownerPublicKey);
|
|
257
|
-
}
|
|
258
|
-
if (!partialHash) {
|
|
259
|
-
const revPubKey = output.revocationCommitment;
|
|
260
|
-
if (revPubKey) {
|
|
261
|
-
if (revPubKey.length === 0) {
|
|
262
|
-
throw new ValidationError(
|
|
263
|
-
`revocation commitment at index ${i} cannot be empty`,
|
|
264
|
-
{
|
|
265
|
-
field: `tokenOutputs[${i}].revocationCommitment`,
|
|
266
|
-
index: i
|
|
267
|
-
}
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
hashObj2.update(revPubKey);
|
|
271
|
-
}
|
|
272
|
-
const bondBytes = new Uint8Array(8);
|
|
273
|
-
new DataView(bondBytes.buffer).setBigUint64(
|
|
274
|
-
0,
|
|
275
|
-
BigInt(output.withdrawBondSats),
|
|
276
|
-
false
|
|
277
|
-
);
|
|
278
|
-
hashObj2.update(bondBytes);
|
|
279
|
-
const locktimeBytes = new Uint8Array(8);
|
|
280
|
-
new DataView(locktimeBytes.buffer).setBigUint64(
|
|
281
|
-
0,
|
|
282
|
-
BigInt(output.withdrawRelativeBlockLocktime),
|
|
283
|
-
false
|
|
284
|
-
);
|
|
285
|
-
hashObj2.update(locktimeBytes);
|
|
286
|
-
}
|
|
287
|
-
if (output.tokenPublicKey) {
|
|
288
|
-
if (output.tokenPublicKey.length === 0) {
|
|
289
|
-
throw new ValidationError(
|
|
290
|
-
`token public key at index ${i} cannot be empty`,
|
|
291
|
-
{
|
|
292
|
-
field: `tokenOutputs[${i}].tokenPublicKey`,
|
|
293
|
-
index: i
|
|
294
|
-
}
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
hashObj2.update(output.tokenPublicKey);
|
|
298
|
-
}
|
|
299
|
-
if (output.tokenAmount) {
|
|
300
|
-
if (output.tokenAmount.length === 0) {
|
|
301
|
-
throw new ValidationError(
|
|
302
|
-
`token amount at index ${i} cannot be empty`,
|
|
303
|
-
{
|
|
304
|
-
field: `tokenOutputs[${i}].tokenAmount`,
|
|
305
|
-
index: i
|
|
306
|
-
}
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
if (output.tokenAmount.length > 16) {
|
|
310
|
-
throw new ValidationError(
|
|
311
|
-
`token amount at index ${i} exceeds maximum length`,
|
|
312
|
-
{
|
|
313
|
-
field: `tokenOutputs[${i}].tokenAmount`,
|
|
314
|
-
value: output.tokenAmount,
|
|
315
|
-
expectedLength: 16,
|
|
316
|
-
actualLength: output.tokenAmount.length,
|
|
317
|
-
index: i
|
|
318
|
-
}
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
hashObj2.update(output.tokenAmount);
|
|
322
|
-
}
|
|
323
|
-
allHashes.push(hashObj2.digest());
|
|
324
|
-
}
|
|
325
|
-
if (!tokenTransaction.sparkOperatorIdentityPublicKeys) {
|
|
326
|
-
throw new ValidationError(
|
|
327
|
-
"spark operator identity public keys cannot be null",
|
|
328
|
-
{}
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
const sortedPubKeys = [
|
|
332
|
-
...tokenTransaction.sparkOperatorIdentityPublicKeys || []
|
|
333
|
-
].sort((a, b) => {
|
|
334
|
-
for (let i = 0; i < a.length && i < b.length; i++) {
|
|
335
|
-
if (a[i] !== b[i]) return a[i] - b[i];
|
|
336
|
-
}
|
|
337
|
-
return a.length - b.length;
|
|
338
|
-
});
|
|
339
|
-
for (const [i, pubKey] of sortedPubKeys.entries()) {
|
|
340
|
-
if (!pubKey) {
|
|
341
|
-
throw new ValidationError(
|
|
342
|
-
`operator public key at index ${i} cannot be null`,
|
|
343
|
-
{
|
|
344
|
-
field: `sparkOperatorIdentityPublicKeys[${i}]`,
|
|
345
|
-
index: i
|
|
346
|
-
}
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
if (pubKey.length === 0) {
|
|
350
|
-
throw new ValidationError(
|
|
351
|
-
`operator public key at index ${i} cannot be empty`,
|
|
352
|
-
{
|
|
353
|
-
field: `sparkOperatorIdentityPublicKeys[${i}]`,
|
|
354
|
-
index: i
|
|
355
|
-
}
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
const hashObj2 = sha256.create();
|
|
359
|
-
hashObj2.update(pubKey);
|
|
360
|
-
allHashes.push(hashObj2.digest());
|
|
361
|
-
}
|
|
362
|
-
const hashObj = sha256.create();
|
|
363
|
-
let networkBytes = new Uint8Array(4);
|
|
364
|
-
new DataView(networkBytes.buffer).setUint32(
|
|
365
|
-
0,
|
|
366
|
-
tokenTransaction.network.valueOf(),
|
|
367
|
-
false
|
|
368
|
-
// false for big-endian
|
|
369
|
-
);
|
|
370
|
-
hashObj.update(networkBytes);
|
|
371
|
-
allHashes.push(hashObj.digest());
|
|
372
|
-
const finalHashObj = sha256.create();
|
|
373
|
-
const concatenatedHashes = new Uint8Array(
|
|
374
|
-
allHashes.reduce((sum, hash) => sum + hash.length, 0)
|
|
375
|
-
);
|
|
376
|
-
let offset = 0;
|
|
377
|
-
for (const hash of allHashes) {
|
|
378
|
-
concatenatedHashes.set(hash, offset);
|
|
379
|
-
offset += hash.length;
|
|
380
|
-
}
|
|
381
|
-
finalHashObj.update(concatenatedHashes);
|
|
382
|
-
return finalHashObj.digest();
|
|
383
|
-
}
|
|
384
|
-
function hashTokenTransactionV1(tokenTransaction, partialHash = false) {
|
|
385
|
-
if (!tokenTransaction) {
|
|
386
|
-
throw new ValidationError("token transaction cannot be nil", {
|
|
387
|
-
field: "tokenTransaction"
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
let allHashes = [];
|
|
391
|
-
const versionHashObj = sha256.create();
|
|
392
|
-
const versionBytes = new Uint8Array(4);
|
|
393
|
-
new DataView(versionBytes.buffer).setUint32(
|
|
394
|
-
0,
|
|
395
|
-
tokenTransaction.version,
|
|
396
|
-
false
|
|
397
|
-
// false for big-endian
|
|
398
|
-
);
|
|
399
|
-
versionHashObj.update(versionBytes);
|
|
400
|
-
allHashes.push(versionHashObj.digest());
|
|
401
|
-
const typeHashObj = sha256.create();
|
|
402
|
-
const typeBytes = new Uint8Array(4);
|
|
403
|
-
let transactionType = 0;
|
|
404
|
-
if (tokenTransaction.tokenInputs?.$case === "mintInput") {
|
|
405
|
-
transactionType = 2 /* TOKEN_TRANSACTION_TYPE_MINT */;
|
|
406
|
-
} else if (tokenTransaction.tokenInputs?.$case === "transferInput") {
|
|
407
|
-
transactionType = 3 /* TOKEN_TRANSACTION_TYPE_TRANSFER */;
|
|
408
|
-
} else if (tokenTransaction.tokenInputs?.$case === "createInput") {
|
|
409
|
-
transactionType = 1 /* TOKEN_TRANSACTION_TYPE_CREATE */;
|
|
410
|
-
} else {
|
|
411
|
-
throw new ValidationError(
|
|
412
|
-
"token transaction must have exactly one input type",
|
|
413
|
-
{
|
|
414
|
-
field: "tokenInputs"
|
|
415
|
-
}
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
new DataView(typeBytes.buffer).setUint32(0, transactionType, false);
|
|
419
|
-
typeHashObj.update(typeBytes);
|
|
420
|
-
allHashes.push(typeHashObj.digest());
|
|
421
|
-
if (tokenTransaction.tokenInputs?.$case === "transferInput") {
|
|
422
|
-
if (!tokenTransaction.tokenInputs.transferInput.outputsToSpend) {
|
|
423
|
-
throw new ValidationError("outputs to spend cannot be null", {
|
|
424
|
-
field: "tokenInputs.transferInput.outputsToSpend"
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
if (tokenTransaction.tokenInputs.transferInput.outputsToSpend.length === 0) {
|
|
428
|
-
throw new ValidationError("outputs to spend cannot be empty", {
|
|
429
|
-
field: "tokenInputs.transferInput.outputsToSpend"
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
const outputsLenHashObj2 = sha256.create();
|
|
433
|
-
const outputsLenBytes2 = new Uint8Array(4);
|
|
434
|
-
new DataView(outputsLenBytes2.buffer).setUint32(
|
|
435
|
-
0,
|
|
436
|
-
tokenTransaction.tokenInputs.transferInput.outputsToSpend.length,
|
|
437
|
-
false
|
|
438
|
-
);
|
|
439
|
-
outputsLenHashObj2.update(outputsLenBytes2);
|
|
440
|
-
allHashes.push(outputsLenHashObj2.digest());
|
|
441
|
-
for (const [
|
|
442
|
-
i,
|
|
443
|
-
output
|
|
444
|
-
] of tokenTransaction.tokenInputs.transferInput.outputsToSpend.entries()) {
|
|
445
|
-
if (!output) {
|
|
446
|
-
throw new ValidationError(`output cannot be null at index ${i}`, {
|
|
447
|
-
field: `tokenInputs.transferInput.outputsToSpend[${i}]`,
|
|
448
|
-
index: i
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
const hashObj2 = sha256.create();
|
|
452
|
-
if (output.prevTokenTransactionHash) {
|
|
453
|
-
const prevHash = output.prevTokenTransactionHash;
|
|
454
|
-
if (output.prevTokenTransactionHash.length !== 32) {
|
|
455
|
-
throw new ValidationError(
|
|
456
|
-
`invalid previous transaction hash length at index ${i}`,
|
|
457
|
-
{
|
|
458
|
-
field: `tokenInputs.transferInput.outputsToSpend[${i}].prevTokenTransactionHash`,
|
|
459
|
-
value: prevHash,
|
|
460
|
-
expectedLength: 32,
|
|
461
|
-
actualLength: prevHash.length,
|
|
462
|
-
index: i
|
|
463
|
-
}
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
hashObj2.update(output.prevTokenTransactionHash);
|
|
467
|
-
}
|
|
468
|
-
const voutBytes = new Uint8Array(4);
|
|
469
|
-
new DataView(voutBytes.buffer).setUint32(
|
|
470
|
-
0,
|
|
471
|
-
output.prevTokenTransactionVout,
|
|
472
|
-
false
|
|
473
|
-
);
|
|
474
|
-
hashObj2.update(voutBytes);
|
|
475
|
-
allHashes.push(hashObj2.digest());
|
|
476
|
-
}
|
|
477
|
-
} else if (tokenTransaction.tokenInputs?.$case === "mintInput") {
|
|
478
|
-
const hashObj2 = sha256.create();
|
|
479
|
-
if (tokenTransaction.tokenInputs.mintInput.issuerPublicKey) {
|
|
480
|
-
const issuerPubKey = tokenTransaction.tokenInputs.mintInput.issuerPublicKey;
|
|
481
|
-
if (issuerPubKey.length === 0) {
|
|
482
|
-
throw new ValidationError("issuer public key cannot be empty", {
|
|
483
|
-
field: "tokenInputs.mintInput.issuerPublicKey",
|
|
484
|
-
value: issuerPubKey,
|
|
485
|
-
expectedLength: 1,
|
|
486
|
-
actualLength: 0
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
hashObj2.update(issuerPubKey);
|
|
490
|
-
allHashes.push(hashObj2.digest());
|
|
491
|
-
const tokenIdentifierHashObj = sha256.create();
|
|
492
|
-
if (tokenTransaction.tokenInputs.mintInput.tokenIdentifier) {
|
|
493
|
-
tokenIdentifierHashObj.update(
|
|
494
|
-
tokenTransaction.tokenInputs.mintInput.tokenIdentifier
|
|
495
|
-
);
|
|
496
|
-
} else {
|
|
497
|
-
tokenIdentifierHashObj.update(new Uint8Array(32));
|
|
498
|
-
}
|
|
499
|
-
allHashes.push(tokenIdentifierHashObj.digest());
|
|
500
|
-
}
|
|
501
|
-
} else if (tokenTransaction.tokenInputs?.$case === "createInput") {
|
|
502
|
-
const createInput = tokenTransaction.tokenInputs.createInput;
|
|
503
|
-
const issuerPubKeyHashObj = sha256.create();
|
|
504
|
-
if (!createInput.issuerPublicKey || createInput.issuerPublicKey.length === 0) {
|
|
505
|
-
throw new ValidationError("issuer public key cannot be nil or empty", {
|
|
506
|
-
field: "tokenInputs.createInput.issuerPublicKey"
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
issuerPubKeyHashObj.update(createInput.issuerPublicKey);
|
|
510
|
-
allHashes.push(issuerPubKeyHashObj.digest());
|
|
511
|
-
const tokenNameHashObj = sha256.create();
|
|
512
|
-
if (!createInput.tokenName || createInput.tokenName.length === 0) {
|
|
513
|
-
throw new ValidationError("token name cannot be empty", {
|
|
514
|
-
field: "tokenInputs.createInput.tokenName"
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
if (createInput.tokenName.length > 20) {
|
|
518
|
-
throw new ValidationError("token name cannot be longer than 20 bytes", {
|
|
519
|
-
field: "tokenInputs.createInput.tokenName",
|
|
520
|
-
value: createInput.tokenName,
|
|
521
|
-
expectedLength: 20,
|
|
522
|
-
actualLength: createInput.tokenName.length
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
const tokenNameEncoder = new TextEncoder();
|
|
526
|
-
tokenNameHashObj.update(tokenNameEncoder.encode(createInput.tokenName));
|
|
527
|
-
allHashes.push(tokenNameHashObj.digest());
|
|
528
|
-
const tokenTickerHashObj = sha256.create();
|
|
529
|
-
if (!createInput.tokenTicker || createInput.tokenTicker.length === 0) {
|
|
530
|
-
throw new ValidationError("token ticker cannot be empty", {
|
|
531
|
-
field: "tokenInputs.createInput.tokenTicker"
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
if (createInput.tokenTicker.length > 6) {
|
|
535
|
-
throw new ValidationError("token ticker cannot be longer than 6 bytes", {
|
|
536
|
-
field: "tokenInputs.createInput.tokenTicker",
|
|
537
|
-
value: createInput.tokenTicker,
|
|
538
|
-
expectedLength: 6,
|
|
539
|
-
actualLength: createInput.tokenTicker.length
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
const tokenTickerEncoder = new TextEncoder();
|
|
543
|
-
tokenTickerHashObj.update(
|
|
544
|
-
tokenTickerEncoder.encode(createInput.tokenTicker)
|
|
545
|
-
);
|
|
546
|
-
allHashes.push(tokenTickerHashObj.digest());
|
|
547
|
-
const decimalsHashObj = sha256.create();
|
|
548
|
-
const decimalsBytes = new Uint8Array(4);
|
|
549
|
-
new DataView(decimalsBytes.buffer).setUint32(
|
|
550
|
-
0,
|
|
551
|
-
createInput.decimals,
|
|
552
|
-
false
|
|
553
|
-
);
|
|
554
|
-
decimalsHashObj.update(decimalsBytes);
|
|
555
|
-
allHashes.push(decimalsHashObj.digest());
|
|
556
|
-
const maxSupplyHashObj = sha256.create();
|
|
557
|
-
if (!createInput.maxSupply) {
|
|
558
|
-
throw new ValidationError("max supply cannot be nil", {
|
|
559
|
-
field: "tokenInputs.createInput.maxSupply"
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
if (createInput.maxSupply.length !== 16) {
|
|
563
|
-
throw new ValidationError("max supply must be exactly 16 bytes", {
|
|
564
|
-
field: "tokenInputs.createInput.maxSupply",
|
|
565
|
-
value: createInput.maxSupply,
|
|
566
|
-
expectedLength: 16,
|
|
567
|
-
actualLength: createInput.maxSupply.length
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
maxSupplyHashObj.update(createInput.maxSupply);
|
|
571
|
-
allHashes.push(maxSupplyHashObj.digest());
|
|
572
|
-
const isFreezableHashObj = sha256.create();
|
|
573
|
-
isFreezableHashObj.update(
|
|
574
|
-
new Uint8Array([createInput.isFreezable ? 1 : 0])
|
|
575
|
-
);
|
|
576
|
-
allHashes.push(isFreezableHashObj.digest());
|
|
577
|
-
const creationEntityHashObj = sha256.create();
|
|
578
|
-
if (!partialHash && createInput.creationEntityPublicKey) {
|
|
579
|
-
creationEntityHashObj.update(createInput.creationEntityPublicKey);
|
|
580
|
-
}
|
|
581
|
-
allHashes.push(creationEntityHashObj.digest());
|
|
582
|
-
}
|
|
583
|
-
if (!tokenTransaction.tokenOutputs) {
|
|
584
|
-
throw new ValidationError("token outputs cannot be null", {
|
|
585
|
-
field: "tokenOutputs"
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
const outputsLenHashObj = sha256.create();
|
|
589
|
-
const outputsLenBytes = new Uint8Array(4);
|
|
590
|
-
new DataView(outputsLenBytes.buffer).setUint32(
|
|
591
|
-
0,
|
|
592
|
-
tokenTransaction.tokenOutputs.length,
|
|
593
|
-
false
|
|
594
|
-
);
|
|
595
|
-
outputsLenHashObj.update(outputsLenBytes);
|
|
596
|
-
allHashes.push(outputsLenHashObj.digest());
|
|
597
|
-
for (const [i, output] of tokenTransaction.tokenOutputs.entries()) {
|
|
598
|
-
if (!output) {
|
|
599
|
-
throw new ValidationError(`output cannot be null at index ${i}`, {
|
|
600
|
-
field: `tokenOutputs[${i}]`,
|
|
601
|
-
index: i
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
const hashObj2 = sha256.create();
|
|
605
|
-
if (output.id && !partialHash) {
|
|
606
|
-
if (output.id.length === 0) {
|
|
607
|
-
throw new ValidationError(`output ID at index ${i} cannot be empty`, {
|
|
608
|
-
field: `tokenOutputs[${i}].id`,
|
|
609
|
-
index: i
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
hashObj2.update(new TextEncoder().encode(output.id));
|
|
613
|
-
}
|
|
614
|
-
if (output.ownerPublicKey) {
|
|
615
|
-
if (output.ownerPublicKey.length === 0) {
|
|
616
|
-
throw new ValidationError(
|
|
617
|
-
`owner public key at index ${i} cannot be empty`,
|
|
618
|
-
{
|
|
619
|
-
field: `tokenOutputs[${i}].ownerPublicKey`,
|
|
620
|
-
index: i
|
|
621
|
-
}
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
hashObj2.update(output.ownerPublicKey);
|
|
625
|
-
}
|
|
626
|
-
if (!partialHash) {
|
|
627
|
-
const revPubKey = output.revocationCommitment;
|
|
628
|
-
if (revPubKey) {
|
|
629
|
-
if (revPubKey.length === 0) {
|
|
630
|
-
throw new ValidationError(
|
|
631
|
-
`revocation commitment at index ${i} cannot be empty`,
|
|
632
|
-
{
|
|
633
|
-
field: `tokenOutputs[${i}].revocationCommitment`,
|
|
634
|
-
index: i
|
|
635
|
-
}
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
hashObj2.update(revPubKey);
|
|
639
|
-
}
|
|
640
|
-
const bondBytes = new Uint8Array(8);
|
|
641
|
-
new DataView(bondBytes.buffer).setBigUint64(
|
|
642
|
-
0,
|
|
643
|
-
BigInt(output.withdrawBondSats),
|
|
644
|
-
false
|
|
645
|
-
);
|
|
646
|
-
hashObj2.update(bondBytes);
|
|
647
|
-
const locktimeBytes = new Uint8Array(8);
|
|
648
|
-
new DataView(locktimeBytes.buffer).setBigUint64(
|
|
649
|
-
0,
|
|
650
|
-
BigInt(output.withdrawRelativeBlockLocktime),
|
|
651
|
-
false
|
|
652
|
-
);
|
|
653
|
-
hashObj2.update(locktimeBytes);
|
|
654
|
-
}
|
|
655
|
-
if (!output.tokenPublicKey || output.tokenPublicKey.length === 0) {
|
|
656
|
-
hashObj2.update(new Uint8Array(33));
|
|
657
|
-
} else {
|
|
658
|
-
hashObj2.update(output.tokenPublicKey);
|
|
659
|
-
}
|
|
660
|
-
if (!output.tokenIdentifier || output.tokenIdentifier.length === 0) {
|
|
661
|
-
hashObj2.update(new Uint8Array(32));
|
|
662
|
-
} else {
|
|
663
|
-
hashObj2.update(output.tokenIdentifier);
|
|
664
|
-
}
|
|
665
|
-
if (output.tokenAmount) {
|
|
666
|
-
if (output.tokenAmount.length === 0) {
|
|
667
|
-
throw new ValidationError(
|
|
668
|
-
`token amount at index ${i} cannot be empty`,
|
|
669
|
-
{
|
|
670
|
-
field: `tokenOutputs[${i}].tokenAmount`,
|
|
671
|
-
index: i
|
|
672
|
-
}
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
if (output.tokenAmount.length > 16) {
|
|
676
|
-
throw new ValidationError(
|
|
677
|
-
`token amount at index ${i} exceeds maximum length`,
|
|
678
|
-
{
|
|
679
|
-
field: `tokenOutputs[${i}].tokenAmount`,
|
|
680
|
-
value: output.tokenAmount,
|
|
681
|
-
expectedLength: 16,
|
|
682
|
-
actualLength: output.tokenAmount.length,
|
|
683
|
-
index: i
|
|
684
|
-
}
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
hashObj2.update(output.tokenAmount);
|
|
688
|
-
}
|
|
689
|
-
allHashes.push(hashObj2.digest());
|
|
690
|
-
}
|
|
691
|
-
if (!tokenTransaction.sparkOperatorIdentityPublicKeys) {
|
|
692
|
-
throw new ValidationError(
|
|
693
|
-
"spark operator identity public keys cannot be null",
|
|
694
|
-
{}
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
const sortedPubKeys = [
|
|
698
|
-
...tokenTransaction.sparkOperatorIdentityPublicKeys || []
|
|
699
|
-
].sort((a, b) => {
|
|
700
|
-
for (let i = 0; i < a.length && i < b.length; i++) {
|
|
701
|
-
if (a[i] !== b[i]) return a[i] - b[i];
|
|
702
|
-
}
|
|
703
|
-
return a.length - b.length;
|
|
704
|
-
});
|
|
705
|
-
const operatorLenHashObj = sha256.create();
|
|
706
|
-
const operatorLenBytes = new Uint8Array(4);
|
|
707
|
-
new DataView(operatorLenBytes.buffer).setUint32(
|
|
708
|
-
0,
|
|
709
|
-
sortedPubKeys.length,
|
|
710
|
-
false
|
|
711
|
-
);
|
|
712
|
-
operatorLenHashObj.update(operatorLenBytes);
|
|
713
|
-
allHashes.push(operatorLenHashObj.digest());
|
|
714
|
-
for (const [i, pubKey] of sortedPubKeys.entries()) {
|
|
715
|
-
if (!pubKey) {
|
|
716
|
-
throw new ValidationError(
|
|
717
|
-
`operator public key at index ${i} cannot be null`,
|
|
718
|
-
{
|
|
719
|
-
field: `sparkOperatorIdentityPublicKeys[${i}]`,
|
|
720
|
-
index: i
|
|
721
|
-
}
|
|
722
|
-
);
|
|
723
|
-
}
|
|
724
|
-
if (pubKey.length === 0) {
|
|
725
|
-
throw new ValidationError(
|
|
726
|
-
`operator public key at index ${i} cannot be empty`,
|
|
727
|
-
{
|
|
728
|
-
field: `sparkOperatorIdentityPublicKeys[${i}]`,
|
|
729
|
-
index: i
|
|
730
|
-
}
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
const hashObj2 = sha256.create();
|
|
734
|
-
hashObj2.update(pubKey);
|
|
735
|
-
allHashes.push(hashObj2.digest());
|
|
736
|
-
}
|
|
737
|
-
const hashObj = sha256.create();
|
|
738
|
-
let networkBytes = new Uint8Array(4);
|
|
739
|
-
new DataView(networkBytes.buffer).setUint32(
|
|
740
|
-
0,
|
|
741
|
-
tokenTransaction.network.valueOf(),
|
|
742
|
-
false
|
|
743
|
-
// false for big-endian
|
|
744
|
-
);
|
|
745
|
-
hashObj.update(networkBytes);
|
|
746
|
-
allHashes.push(hashObj.digest());
|
|
747
|
-
const clientTimestampHashObj = sha256.create();
|
|
748
|
-
const clientCreatedTs = tokenTransaction.clientCreatedTimestamp;
|
|
749
|
-
if (!clientCreatedTs) {
|
|
750
|
-
throw new ValidationError(
|
|
751
|
-
"client created timestamp cannot be null for V1 token transactions",
|
|
752
|
-
{
|
|
753
|
-
field: "clientCreatedTimestamp"
|
|
754
|
-
}
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
const clientUnixTime = clientCreatedTs.getTime();
|
|
758
|
-
const clientTimestampBytes = new Uint8Array(8);
|
|
759
|
-
new DataView(clientTimestampBytes.buffer).setBigUint64(
|
|
760
|
-
0,
|
|
761
|
-
BigInt(clientUnixTime),
|
|
762
|
-
false
|
|
763
|
-
);
|
|
764
|
-
clientTimestampHashObj.update(clientTimestampBytes);
|
|
765
|
-
allHashes.push(clientTimestampHashObj.digest());
|
|
766
|
-
if (!partialHash) {
|
|
767
|
-
const expiryHashObj = sha256.create();
|
|
768
|
-
const expiryTimeBytes = new Uint8Array(8);
|
|
769
|
-
const expiryUnixTime = tokenTransaction.expiryTime ? Math.floor(tokenTransaction.expiryTime.getTime() / 1e3) : 0;
|
|
770
|
-
new DataView(expiryTimeBytes.buffer).setBigUint64(
|
|
771
|
-
0,
|
|
772
|
-
BigInt(expiryUnixTime),
|
|
773
|
-
false
|
|
774
|
-
// false for big-endian
|
|
775
|
-
);
|
|
776
|
-
expiryHashObj.update(expiryTimeBytes);
|
|
777
|
-
allHashes.push(expiryHashObj.digest());
|
|
778
|
-
}
|
|
779
|
-
const finalHashObj = sha256.create();
|
|
780
|
-
const concatenatedHashes = new Uint8Array(
|
|
781
|
-
allHashes.reduce((sum, hash) => sum + hash.length, 0)
|
|
782
|
-
);
|
|
783
|
-
let offset = 0;
|
|
784
|
-
for (const hash of allHashes) {
|
|
785
|
-
concatenatedHashes.set(hash, offset);
|
|
786
|
-
offset += hash.length;
|
|
787
|
-
}
|
|
788
|
-
finalHashObj.update(concatenatedHashes);
|
|
789
|
-
return finalHashObj.digest();
|
|
790
|
-
}
|
|
791
|
-
function hashOperatorSpecificTokenTransactionSignablePayload(payload) {
|
|
792
|
-
if (!payload) {
|
|
793
|
-
throw new ValidationError(
|
|
794
|
-
"operator specific token transaction signable payload cannot be null",
|
|
795
|
-
{
|
|
796
|
-
field: "payload"
|
|
797
|
-
}
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
let allHashes = [];
|
|
801
|
-
if (payload.finalTokenTransactionHash) {
|
|
802
|
-
const hashObj2 = sha256.create();
|
|
803
|
-
if (payload.finalTokenTransactionHash.length !== 32) {
|
|
804
|
-
throw new ValidationError(`invalid final token transaction hash length`, {
|
|
805
|
-
field: "finalTokenTransactionHash",
|
|
806
|
-
value: payload.finalTokenTransactionHash,
|
|
807
|
-
expectedLength: 32,
|
|
808
|
-
actualLength: payload.finalTokenTransactionHash.length
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
hashObj2.update(payload.finalTokenTransactionHash);
|
|
812
|
-
allHashes.push(hashObj2.digest());
|
|
813
|
-
}
|
|
814
|
-
if (!payload.operatorIdentityPublicKey) {
|
|
815
|
-
throw new ValidationError("operator identity public key cannot be null", {
|
|
816
|
-
field: "operatorIdentityPublicKey"
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
if (payload.operatorIdentityPublicKey.length === 0) {
|
|
820
|
-
throw new ValidationError("operator identity public key cannot be empty", {
|
|
821
|
-
field: "operatorIdentityPublicKey"
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
const hashObj = sha256.create();
|
|
825
|
-
hashObj.update(payload.operatorIdentityPublicKey);
|
|
826
|
-
allHashes.push(hashObj.digest());
|
|
827
|
-
const finalHashObj = sha256.create();
|
|
828
|
-
const concatenatedHashes = new Uint8Array(
|
|
829
|
-
allHashes.reduce((sum, hash) => sum + hash.length, 0)
|
|
830
|
-
);
|
|
831
|
-
let offset = 0;
|
|
832
|
-
for (const hash of allHashes) {
|
|
833
|
-
concatenatedHashes.set(hash, offset);
|
|
834
|
-
offset += hash.length;
|
|
835
|
-
}
|
|
836
|
-
finalHashObj.update(concatenatedHashes);
|
|
837
|
-
return finalHashObj.digest();
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// src/utils/token-transaction-validation.ts
|
|
841
|
-
function areByteArraysEqual(a, b) {
|
|
842
|
-
if (a.length !== b.length) {
|
|
843
|
-
return false;
|
|
844
|
-
}
|
|
845
|
-
return a.every((byte, index) => byte === b[index]);
|
|
846
|
-
}
|
|
847
|
-
function hasDuplicates(array) {
|
|
848
|
-
return new Set(array).size !== array.length;
|
|
849
|
-
}
|
|
850
|
-
function validateTokenTransactionV0(finalTokenTransaction, partialTokenTransaction, signingOperators, keyshareInfo, expectedWithdrawBondSats, expectedWithdrawRelativeBlockLocktime, expectedThreshold) {
|
|
851
|
-
if (finalTokenTransaction.network !== partialTokenTransaction.network) {
|
|
852
|
-
throw new InternalValidationError(
|
|
853
|
-
"Network mismatch in response token transaction",
|
|
854
|
-
{
|
|
855
|
-
value: finalTokenTransaction.network,
|
|
856
|
-
expected: partialTokenTransaction.network
|
|
857
|
-
}
|
|
858
|
-
);
|
|
859
|
-
}
|
|
860
|
-
if (!finalTokenTransaction.tokenInputs) {
|
|
861
|
-
throw new InternalValidationError(
|
|
862
|
-
"Token inputs missing in final transaction",
|
|
863
|
-
{
|
|
864
|
-
value: finalTokenTransaction
|
|
865
|
-
}
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
if (!partialTokenTransaction.tokenInputs) {
|
|
869
|
-
throw new InternalValidationError(
|
|
870
|
-
"Token inputs missing in partial transaction",
|
|
871
|
-
{
|
|
872
|
-
value: partialTokenTransaction
|
|
873
|
-
}
|
|
874
|
-
);
|
|
875
|
-
}
|
|
876
|
-
if (finalTokenTransaction.tokenInputs.$case !== partialTokenTransaction.tokenInputs.$case) {
|
|
877
|
-
throw new InternalValidationError(
|
|
878
|
-
`Transaction type mismatch: final transaction has ${finalTokenTransaction.tokenInputs.$case}, partial transaction has ${partialTokenTransaction.tokenInputs.$case}`,
|
|
879
|
-
{
|
|
880
|
-
value: finalTokenTransaction.tokenInputs.$case,
|
|
881
|
-
expected: partialTokenTransaction.tokenInputs.$case
|
|
882
|
-
}
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
if (finalTokenTransaction.sparkOperatorIdentityPublicKeys.length !== partialTokenTransaction.sparkOperatorIdentityPublicKeys.length) {
|
|
886
|
-
throw new InternalValidationError(
|
|
887
|
-
"Spark operator identity public keys count mismatch",
|
|
888
|
-
{
|
|
889
|
-
value: finalTokenTransaction.sparkOperatorIdentityPublicKeys.length,
|
|
890
|
-
expected: partialTokenTransaction.sparkOperatorIdentityPublicKeys.length
|
|
891
|
-
}
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
if (partialTokenTransaction.tokenInputs.$case === "mintInput" && finalTokenTransaction.tokenInputs.$case === "mintInput") {
|
|
895
|
-
const finalMintInput = finalTokenTransaction.tokenInputs.mintInput;
|
|
896
|
-
const partialMintInput = partialTokenTransaction.tokenInputs.mintInput;
|
|
897
|
-
if (!areByteArraysEqual(
|
|
898
|
-
finalMintInput.issuerPublicKey,
|
|
899
|
-
partialMintInput.issuerPublicKey
|
|
900
|
-
)) {
|
|
901
|
-
throw new InternalValidationError(
|
|
902
|
-
"Issuer public key mismatch in mint input",
|
|
903
|
-
{
|
|
904
|
-
value: finalMintInput.issuerPublicKey.toString(),
|
|
905
|
-
expected: partialMintInput.issuerPublicKey.toString()
|
|
906
|
-
}
|
|
907
|
-
);
|
|
908
|
-
}
|
|
909
|
-
} else if (partialTokenTransaction.tokenInputs.$case === "transferInput" && finalTokenTransaction.tokenInputs.$case === "transferInput") {
|
|
910
|
-
const finalTransferInput = finalTokenTransaction.tokenInputs.transferInput;
|
|
911
|
-
const partialTransferInput = partialTokenTransaction.tokenInputs.transferInput;
|
|
912
|
-
if (finalTransferInput.outputsToSpend.length !== partialTransferInput.outputsToSpend.length) {
|
|
913
|
-
throw new InternalValidationError(
|
|
914
|
-
"Outputs to spend count mismatch in transfer input",
|
|
915
|
-
{
|
|
916
|
-
value: finalTransferInput.outputsToSpend.length,
|
|
917
|
-
expected: partialTransferInput.outputsToSpend.length
|
|
918
|
-
}
|
|
919
|
-
);
|
|
920
|
-
}
|
|
921
|
-
for (let i = 0; i < finalTransferInput.outputsToSpend.length; i++) {
|
|
922
|
-
const finalOutput = finalTransferInput.outputsToSpend[i];
|
|
923
|
-
const partialOutput = partialTransferInput.outputsToSpend[i];
|
|
924
|
-
if (!finalOutput) {
|
|
925
|
-
throw new InternalValidationError(
|
|
926
|
-
"Token output to spend missing in final transaction",
|
|
927
|
-
{
|
|
928
|
-
outputIndex: i,
|
|
929
|
-
value: finalOutput
|
|
930
|
-
}
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
if (!partialOutput) {
|
|
934
|
-
throw new InternalValidationError(
|
|
935
|
-
"Token output to spend missing in partial transaction",
|
|
936
|
-
{
|
|
937
|
-
outputIndex: i,
|
|
938
|
-
value: partialOutput
|
|
939
|
-
}
|
|
940
|
-
);
|
|
941
|
-
}
|
|
942
|
-
if (!areByteArraysEqual(
|
|
943
|
-
finalOutput.prevTokenTransactionHash,
|
|
944
|
-
partialOutput.prevTokenTransactionHash
|
|
945
|
-
)) {
|
|
946
|
-
throw new InternalValidationError(
|
|
947
|
-
"Previous token transaction hash mismatch in transfer input",
|
|
948
|
-
{
|
|
949
|
-
outputIndex: i,
|
|
950
|
-
value: finalOutput.prevTokenTransactionHash.toString(),
|
|
951
|
-
expected: partialOutput.prevTokenTransactionHash.toString()
|
|
952
|
-
}
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
if (finalOutput.prevTokenTransactionVout !== partialOutput.prevTokenTransactionVout) {
|
|
956
|
-
throw new InternalValidationError(
|
|
957
|
-
"Previous token transaction vout mismatch in transfer input",
|
|
958
|
-
{
|
|
959
|
-
outputIndex: i,
|
|
960
|
-
value: finalOutput.prevTokenTransactionVout,
|
|
961
|
-
expected: partialOutput.prevTokenTransactionVout
|
|
962
|
-
}
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
if (finalTokenTransaction.tokenOutputs.length !== partialTokenTransaction.tokenOutputs.length) {
|
|
968
|
-
throw new InternalValidationError("Token outputs count mismatch", {
|
|
969
|
-
value: finalTokenTransaction.tokenOutputs.length,
|
|
970
|
-
expected: partialTokenTransaction.tokenOutputs.length
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
for (let i = 0; i < finalTokenTransaction.tokenOutputs.length; i++) {
|
|
974
|
-
const finalOutput = finalTokenTransaction.tokenOutputs[i];
|
|
975
|
-
const partialOutput = partialTokenTransaction.tokenOutputs[i];
|
|
976
|
-
if (!finalOutput) {
|
|
977
|
-
throw new InternalValidationError(
|
|
978
|
-
"Token output missing in final transaction",
|
|
979
|
-
{
|
|
980
|
-
outputIndex: i,
|
|
981
|
-
value: finalOutput
|
|
982
|
-
}
|
|
983
|
-
);
|
|
984
|
-
}
|
|
985
|
-
if (!partialOutput) {
|
|
986
|
-
throw new InternalValidationError(
|
|
987
|
-
"Token output missing in partial transaction",
|
|
988
|
-
{
|
|
989
|
-
outputIndex: i,
|
|
990
|
-
value: partialOutput
|
|
991
|
-
}
|
|
992
|
-
);
|
|
993
|
-
}
|
|
994
|
-
if (!areByteArraysEqual(
|
|
995
|
-
finalOutput.ownerPublicKey,
|
|
996
|
-
partialOutput.ownerPublicKey
|
|
997
|
-
)) {
|
|
998
|
-
throw new InternalValidationError(
|
|
999
|
-
"Owner public key mismatch in token output",
|
|
1000
|
-
{
|
|
1001
|
-
outputIndex: i,
|
|
1002
|
-
value: finalOutput.ownerPublicKey.toString(),
|
|
1003
|
-
expected: partialOutput.ownerPublicKey.toString()
|
|
1004
|
-
}
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
if (finalOutput.tokenPublicKey !== void 0 && partialOutput.tokenPublicKey !== void 0 && !areByteArraysEqual(
|
|
1008
|
-
finalOutput.tokenPublicKey,
|
|
1009
|
-
partialOutput.tokenPublicKey
|
|
1010
|
-
)) {
|
|
1011
|
-
throw new InternalValidationError(
|
|
1012
|
-
"Token public key mismatch in token output",
|
|
1013
|
-
{
|
|
1014
|
-
outputIndex: i,
|
|
1015
|
-
value: finalOutput.tokenPublicKey?.toString(),
|
|
1016
|
-
expected: partialOutput.tokenPublicKey?.toString()
|
|
1017
|
-
}
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
if (!areByteArraysEqual(finalOutput.tokenAmount, partialOutput.tokenAmount)) {
|
|
1021
|
-
throw new InternalValidationError(
|
|
1022
|
-
"Token amount mismatch in token output",
|
|
1023
|
-
{
|
|
1024
|
-
outputIndex: i,
|
|
1025
|
-
value: finalOutput.tokenAmount.toString(),
|
|
1026
|
-
expected: partialOutput.tokenAmount.toString()
|
|
1027
|
-
}
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
if (finalOutput.withdrawBondSats !== void 0) {
|
|
1031
|
-
if (finalOutput.withdrawBondSats !== expectedWithdrawBondSats) {
|
|
1032
|
-
throw new InternalValidationError(
|
|
1033
|
-
"Withdraw bond sats mismatch in token output",
|
|
1034
|
-
{
|
|
1035
|
-
outputIndex: i,
|
|
1036
|
-
value: finalOutput.withdrawBondSats,
|
|
1037
|
-
expected: expectedWithdrawBondSats
|
|
1038
|
-
}
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
if (finalOutput.withdrawRelativeBlockLocktime !== void 0) {
|
|
1043
|
-
if (finalOutput.withdrawRelativeBlockLocktime !== expectedWithdrawRelativeBlockLocktime) {
|
|
1044
|
-
throw new InternalValidationError(
|
|
1045
|
-
"Withdraw relative block locktime mismatch in token output",
|
|
1046
|
-
{
|
|
1047
|
-
outputIndex: i,
|
|
1048
|
-
value: finalOutput.withdrawRelativeBlockLocktime,
|
|
1049
|
-
expected: expectedWithdrawRelativeBlockLocktime
|
|
1050
|
-
}
|
|
1051
|
-
);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
if (keyshareInfo.threshold !== expectedThreshold) {
|
|
1055
|
-
throw new InternalValidationError(
|
|
1056
|
-
"Threshold mismatch: expected " + expectedThreshold + " but got " + keyshareInfo.threshold,
|
|
1057
|
-
{
|
|
1058
|
-
field: "threshold",
|
|
1059
|
-
value: keyshareInfo.threshold,
|
|
1060
|
-
expected: expectedThreshold
|
|
1061
|
-
}
|
|
1062
|
-
);
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
if (keyshareInfo.ownerIdentifiers.length !== Object.keys(signingOperators).length) {
|
|
1066
|
-
throw new InternalValidationError(
|
|
1067
|
-
`Keyshare operator count (${keyshareInfo.ownerIdentifiers.length}) does not match signing operator count (${Object.keys(signingOperators).length})`,
|
|
1068
|
-
{
|
|
1069
|
-
keyshareInfo: keyshareInfo.ownerIdentifiers.length,
|
|
1070
|
-
signingOperators: Object.keys(signingOperators).length
|
|
1071
|
-
}
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
|
-
if (hasDuplicates(keyshareInfo.ownerIdentifiers)) {
|
|
1075
|
-
throw new InternalValidationError(
|
|
1076
|
-
"Duplicate ownerIdentifiers found in keyshareInfo",
|
|
1077
|
-
{
|
|
1078
|
-
keyshareInfo: keyshareInfo.ownerIdentifiers
|
|
1079
|
-
}
|
|
1080
|
-
);
|
|
1081
|
-
}
|
|
1082
|
-
for (const identifier of keyshareInfo.ownerIdentifiers) {
|
|
1083
|
-
if (!signingOperators[identifier]) {
|
|
1084
|
-
throw new InternalValidationError(
|
|
1085
|
-
`Keyshare operator ${identifier} not found in signing operator list`,
|
|
1086
|
-
{
|
|
1087
|
-
keyshareInfo: identifier,
|
|
1088
|
-
signingOperators: Object.keys(signingOperators)
|
|
1089
|
-
}
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
function validateTokenTransaction(finalTokenTransaction, partialTokenTransaction, signingOperators, keyshareInfo, expectedWithdrawBondSats, expectedWithdrawRelativeBlockLocktime, expectedThreshold) {
|
|
1095
|
-
if (finalTokenTransaction.network !== partialTokenTransaction.network) {
|
|
1096
|
-
throw new InternalValidationError(
|
|
1097
|
-
"Network mismatch in response token transaction",
|
|
1098
|
-
{
|
|
1099
|
-
value: finalTokenTransaction.network,
|
|
1100
|
-
expected: partialTokenTransaction.network
|
|
1101
|
-
}
|
|
1102
|
-
);
|
|
1103
|
-
}
|
|
1104
|
-
if (!finalTokenTransaction.tokenInputs) {
|
|
1105
|
-
throw new InternalValidationError(
|
|
1106
|
-
"Token inputs missing in final transaction",
|
|
1107
|
-
{
|
|
1108
|
-
value: finalTokenTransaction
|
|
1109
|
-
}
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
if (!partialTokenTransaction.tokenInputs) {
|
|
1113
|
-
throw new InternalValidationError(
|
|
1114
|
-
"Token inputs missing in partial transaction",
|
|
1115
|
-
{
|
|
1116
|
-
value: partialTokenTransaction
|
|
1117
|
-
}
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
if (finalTokenTransaction.tokenInputs.$case !== partialTokenTransaction.tokenInputs.$case) {
|
|
1121
|
-
throw new InternalValidationError(
|
|
1122
|
-
`Transaction type mismatch: final transaction has ${finalTokenTransaction.tokenInputs.$case}, partial transaction has ${partialTokenTransaction.tokenInputs.$case}`,
|
|
1123
|
-
{
|
|
1124
|
-
value: finalTokenTransaction.tokenInputs.$case,
|
|
1125
|
-
expected: partialTokenTransaction.tokenInputs.$case
|
|
1126
|
-
}
|
|
1127
|
-
);
|
|
1128
|
-
}
|
|
1129
|
-
if (finalTokenTransaction.sparkOperatorIdentityPublicKeys.length !== partialTokenTransaction.sparkOperatorIdentityPublicKeys.length) {
|
|
1130
|
-
throw new InternalValidationError(
|
|
1131
|
-
"Spark operator identity public keys count mismatch",
|
|
1132
|
-
{
|
|
1133
|
-
value: finalTokenTransaction.sparkOperatorIdentityPublicKeys.length,
|
|
1134
|
-
expected: partialTokenTransaction.sparkOperatorIdentityPublicKeys.length
|
|
1135
|
-
}
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
if (partialTokenTransaction.tokenInputs.$case === "mintInput" && finalTokenTransaction.tokenInputs.$case === "mintInput") {
|
|
1139
|
-
const finalMintInput = finalTokenTransaction.tokenInputs.mintInput;
|
|
1140
|
-
const partialMintInput = partialTokenTransaction.tokenInputs.mintInput;
|
|
1141
|
-
if (!areByteArraysEqual(
|
|
1142
|
-
finalMintInput.issuerPublicKey,
|
|
1143
|
-
partialMintInput.issuerPublicKey
|
|
1144
|
-
)) {
|
|
1145
|
-
throw new InternalValidationError(
|
|
1146
|
-
"Issuer public key mismatch in mint input",
|
|
1147
|
-
{
|
|
1148
|
-
value: finalMintInput.issuerPublicKey.toString(),
|
|
1149
|
-
expected: partialMintInput.issuerPublicKey.toString()
|
|
1150
|
-
}
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
} else if (partialTokenTransaction.tokenInputs.$case === "transferInput" && finalTokenTransaction.tokenInputs.$case === "transferInput") {
|
|
1154
|
-
const finalTransferInput = finalTokenTransaction.tokenInputs.transferInput;
|
|
1155
|
-
const partialTransferInput = partialTokenTransaction.tokenInputs.transferInput;
|
|
1156
|
-
if (finalTransferInput.outputsToSpend.length !== partialTransferInput.outputsToSpend.length) {
|
|
1157
|
-
throw new InternalValidationError(
|
|
1158
|
-
"Outputs to spend count mismatch in transfer input",
|
|
1159
|
-
{
|
|
1160
|
-
value: finalTransferInput.outputsToSpend.length,
|
|
1161
|
-
expected: partialTransferInput.outputsToSpend.length
|
|
1162
|
-
}
|
|
1163
|
-
);
|
|
1164
|
-
}
|
|
1165
|
-
for (let i = 0; i < finalTransferInput.outputsToSpend.length; i++) {
|
|
1166
|
-
const finalOutput = finalTransferInput.outputsToSpend[i];
|
|
1167
|
-
const partialOutput = partialTransferInput.outputsToSpend[i];
|
|
1168
|
-
if (!finalOutput) {
|
|
1169
|
-
throw new InternalValidationError(
|
|
1170
|
-
"Token output to spend missing in final transaction",
|
|
1171
|
-
{
|
|
1172
|
-
outputIndex: i,
|
|
1173
|
-
value: finalOutput
|
|
1174
|
-
}
|
|
1175
|
-
);
|
|
1176
|
-
}
|
|
1177
|
-
if (!partialOutput) {
|
|
1178
|
-
throw new InternalValidationError(
|
|
1179
|
-
"Token output to spend missing in partial transaction",
|
|
1180
|
-
{
|
|
1181
|
-
outputIndex: i,
|
|
1182
|
-
value: partialOutput
|
|
1183
|
-
}
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
if (!areByteArraysEqual(
|
|
1187
|
-
finalOutput.prevTokenTransactionHash,
|
|
1188
|
-
partialOutput.prevTokenTransactionHash
|
|
1189
|
-
)) {
|
|
1190
|
-
throw new InternalValidationError(
|
|
1191
|
-
"Previous token transaction hash mismatch in transfer input",
|
|
1192
|
-
{
|
|
1193
|
-
outputIndex: i,
|
|
1194
|
-
value: finalOutput.prevTokenTransactionHash.toString(),
|
|
1195
|
-
expected: partialOutput.prevTokenTransactionHash.toString()
|
|
1196
|
-
}
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
if (finalOutput.prevTokenTransactionVout !== partialOutput.prevTokenTransactionVout) {
|
|
1200
|
-
throw new InternalValidationError(
|
|
1201
|
-
"Previous token transaction vout mismatch in transfer input",
|
|
1202
|
-
{
|
|
1203
|
-
outputIndex: i,
|
|
1204
|
-
value: finalOutput.prevTokenTransactionVout,
|
|
1205
|
-
expected: partialOutput.prevTokenTransactionVout
|
|
1206
|
-
}
|
|
1207
|
-
);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
if (finalTokenTransaction.tokenOutputs.length !== partialTokenTransaction.tokenOutputs.length) {
|
|
1212
|
-
throw new InternalValidationError("Token outputs count mismatch", {
|
|
1213
|
-
value: finalTokenTransaction.tokenOutputs.length,
|
|
1214
|
-
expected: partialTokenTransaction.tokenOutputs.length
|
|
1215
|
-
});
|
|
1216
|
-
}
|
|
1217
|
-
for (let i = 0; i < finalTokenTransaction.tokenOutputs.length; i++) {
|
|
1218
|
-
const finalOutput = finalTokenTransaction.tokenOutputs[i];
|
|
1219
|
-
const partialOutput = partialTokenTransaction.tokenOutputs[i];
|
|
1220
|
-
if (!finalOutput) {
|
|
1221
|
-
throw new InternalValidationError(
|
|
1222
|
-
"Token output missing in final transaction",
|
|
1223
|
-
{
|
|
1224
|
-
outputIndex: i,
|
|
1225
|
-
value: finalOutput
|
|
1226
|
-
}
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
if (!partialOutput) {
|
|
1230
|
-
throw new InternalValidationError(
|
|
1231
|
-
"Token output missing in partial transaction",
|
|
1232
|
-
{
|
|
1233
|
-
outputIndex: i,
|
|
1234
|
-
value: partialOutput
|
|
1235
|
-
}
|
|
1236
|
-
);
|
|
1237
|
-
}
|
|
1238
|
-
if (!areByteArraysEqual(
|
|
1239
|
-
finalOutput.ownerPublicKey,
|
|
1240
|
-
partialOutput.ownerPublicKey
|
|
1241
|
-
)) {
|
|
1242
|
-
throw new InternalValidationError(
|
|
1243
|
-
"Owner public key mismatch in token output",
|
|
1244
|
-
{
|
|
1245
|
-
outputIndex: i,
|
|
1246
|
-
value: finalOutput.ownerPublicKey.toString(),
|
|
1247
|
-
expected: partialOutput.ownerPublicKey.toString()
|
|
1248
|
-
}
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1251
|
-
if (finalOutput.tokenPublicKey !== void 0 && partialOutput.tokenPublicKey !== void 0 && !areByteArraysEqual(
|
|
1252
|
-
finalOutput.tokenPublicKey,
|
|
1253
|
-
partialOutput.tokenPublicKey
|
|
1254
|
-
)) {
|
|
1255
|
-
throw new InternalValidationError(
|
|
1256
|
-
"Token public key mismatch in token output",
|
|
1257
|
-
{
|
|
1258
|
-
outputIndex: i,
|
|
1259
|
-
value: finalOutput.tokenPublicKey.toString(),
|
|
1260
|
-
expected: partialOutput.tokenPublicKey.toString()
|
|
1261
|
-
}
|
|
1262
|
-
);
|
|
1263
|
-
}
|
|
1264
|
-
if (!areByteArraysEqual(finalOutput.tokenAmount, partialOutput.tokenAmount)) {
|
|
1265
|
-
throw new InternalValidationError(
|
|
1266
|
-
"Token amount mismatch in token output",
|
|
1267
|
-
{
|
|
1268
|
-
outputIndex: i,
|
|
1269
|
-
value: finalOutput.tokenAmount.toString(),
|
|
1270
|
-
expected: partialOutput.tokenAmount.toString()
|
|
1271
|
-
}
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
if (finalOutput.withdrawBondSats !== void 0) {
|
|
1275
|
-
if (finalOutput.withdrawBondSats !== expectedWithdrawBondSats) {
|
|
1276
|
-
throw new InternalValidationError(
|
|
1277
|
-
"Withdraw bond sats mismatch in token output",
|
|
1278
|
-
{
|
|
1279
|
-
outputIndex: i,
|
|
1280
|
-
value: finalOutput.withdrawBondSats,
|
|
1281
|
-
expected: expectedWithdrawBondSats
|
|
1282
|
-
}
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
if (finalOutput.withdrawRelativeBlockLocktime !== void 0) {
|
|
1287
|
-
if (finalOutput.withdrawRelativeBlockLocktime !== expectedWithdrawRelativeBlockLocktime) {
|
|
1288
|
-
throw new InternalValidationError(
|
|
1289
|
-
"Withdraw relative block locktime mismatch in token output",
|
|
1290
|
-
{
|
|
1291
|
-
outputIndex: i,
|
|
1292
|
-
value: finalOutput.withdrawRelativeBlockLocktime,
|
|
1293
|
-
expected: expectedWithdrawRelativeBlockLocktime
|
|
1294
|
-
}
|
|
1295
|
-
);
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
if (keyshareInfo.threshold !== expectedThreshold) {
|
|
1299
|
-
throw new InternalValidationError(
|
|
1300
|
-
"Threshold mismatch: expected " + expectedThreshold + " but got " + keyshareInfo.threshold,
|
|
1301
|
-
{
|
|
1302
|
-
field: "threshold",
|
|
1303
|
-
value: keyshareInfo.threshold,
|
|
1304
|
-
expected: expectedThreshold
|
|
1305
|
-
}
|
|
1306
|
-
);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
if (keyshareInfo.ownerIdentifiers.length !== Object.keys(signingOperators).length) {
|
|
1310
|
-
throw new InternalValidationError(
|
|
1311
|
-
`Keyshare operator count (${keyshareInfo.ownerIdentifiers.length}) does not match signing operator count (${Object.keys(signingOperators).length})`,
|
|
1312
|
-
{
|
|
1313
|
-
keyshareInfo: keyshareInfo.ownerIdentifiers.length,
|
|
1314
|
-
signingOperators: Object.keys(signingOperators).length
|
|
1315
|
-
}
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
if (hasDuplicates(keyshareInfo.ownerIdentifiers)) {
|
|
1319
|
-
throw new InternalValidationError(
|
|
1320
|
-
"Duplicate ownerIdentifiers found in keyshareInfo",
|
|
1321
|
-
{
|
|
1322
|
-
keyshareInfo: keyshareInfo.ownerIdentifiers
|
|
1323
|
-
}
|
|
1324
|
-
);
|
|
1325
|
-
}
|
|
1326
|
-
for (const identifier of keyshareInfo.ownerIdentifiers) {
|
|
1327
|
-
if (!signingOperators[identifier]) {
|
|
1328
|
-
throw new InternalValidationError(
|
|
1329
|
-
`Keyshare operator ${identifier} not found in signing operator list`,
|
|
1330
|
-
{
|
|
1331
|
-
keyshareInfo: identifier,
|
|
1332
|
-
signingOperators: Object.keys(signingOperators)
|
|
1333
|
-
}
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
if (finalTokenTransaction.clientCreatedTimestamp.getTime() !== partialTokenTransaction.clientCreatedTimestamp.getTime()) {
|
|
1338
|
-
throw new InternalValidationError("Client created timestamp mismatch", {
|
|
1339
|
-
value: finalTokenTransaction.clientCreatedTimestamp,
|
|
1340
|
-
expected: partialTokenTransaction.clientCreatedTimestamp
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// src/services/token-transactions.ts
|
|
1346
|
-
import { hexToBytes } from "@noble/hashes/utils";
|
|
1347
|
-
|
|
1348
|
-
// src/utils/token-keyshares.ts
|
|
1349
|
-
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
1350
|
-
function recoverRevocationSecretFromKeyshares(keyshares, threshold) {
|
|
1351
|
-
const shares = keyshares.map((keyshare) => ({
|
|
1352
|
-
fieldModulus: BigInt("0x" + secp256k1.CURVE.n.toString(16)),
|
|
1353
|
-
// secp256k1 curve order
|
|
1354
|
-
threshold,
|
|
1355
|
-
index: BigInt(keyshare.operatorIndex),
|
|
1356
|
-
share: BigInt(
|
|
1357
|
-
"0x" + Buffer.from(keyshare.keyshare.keyshare).toString("hex")
|
|
1358
|
-
),
|
|
1359
|
-
proofs: []
|
|
1360
|
-
}));
|
|
1361
|
-
const recoveredSecret = recoverSecret(shares);
|
|
1362
|
-
return bigIntToPrivateKey(recoveredSecret);
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
// src/services/token-transactions.ts
|
|
1366
|
-
var MAX_TOKEN_OUTPUTS = 500;
|
|
1367
|
-
var TokenTransactionService = class {
|
|
1368
|
-
config;
|
|
1369
|
-
connectionManager;
|
|
1370
|
-
constructor(config, connectionManager) {
|
|
1371
|
-
this.config = config;
|
|
1372
|
-
this.connectionManager = connectionManager;
|
|
1373
|
-
}
|
|
1374
|
-
async tokenTransfer(tokenOutputs, receiverOutputs, outputSelectionStrategy = "SMALL_FIRST", selectedOutputs) {
|
|
1375
|
-
if (receiverOutputs.length === 0) {
|
|
1376
|
-
throw new ValidationError("No receiver outputs provided", {
|
|
1377
|
-
field: "receiverOutputs",
|
|
1378
|
-
value: receiverOutputs,
|
|
1379
|
-
expected: "Non-empty array"
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
|
-
const totalTokenAmount = receiverOutputs.reduce(
|
|
1383
|
-
(sum, transfer) => sum + transfer.tokenAmount,
|
|
1384
|
-
0n
|
|
1385
|
-
);
|
|
1386
|
-
let outputsToUse;
|
|
1387
|
-
if (selectedOutputs) {
|
|
1388
|
-
outputsToUse = selectedOutputs;
|
|
1389
|
-
if (!checkIfSelectedOutputsAreAvailable(
|
|
1390
|
-
outputsToUse,
|
|
1391
|
-
tokenOutputs,
|
|
1392
|
-
hexToBytes(receiverOutputs[0].tokenPublicKey)
|
|
1393
|
-
)) {
|
|
1394
|
-
throw new ValidationError(
|
|
1395
|
-
"One or more selected TTXOs are not available",
|
|
1396
|
-
{
|
|
1397
|
-
field: "selectedOutputs",
|
|
1398
|
-
value: selectedOutputs,
|
|
1399
|
-
expected: "Available TTXOs"
|
|
1400
|
-
}
|
|
1401
|
-
);
|
|
1402
|
-
}
|
|
1403
|
-
} else {
|
|
1404
|
-
outputsToUse = this.selectTokenOutputs(
|
|
1405
|
-
tokenOutputs.get(receiverOutputs[0].tokenPublicKey),
|
|
1406
|
-
totalTokenAmount,
|
|
1407
|
-
outputSelectionStrategy
|
|
1408
|
-
);
|
|
1409
|
-
}
|
|
1410
|
-
if (outputsToUse.length > MAX_TOKEN_OUTPUTS) {
|
|
1411
|
-
const availableOutputs = tokenOutputs.get(
|
|
1412
|
-
receiverOutputs[0].tokenPublicKey
|
|
1413
|
-
);
|
|
1414
|
-
const sortedOutputs = [...availableOutputs];
|
|
1415
|
-
this.sortTokenOutputsByStrategy(sortedOutputs, outputSelectionStrategy);
|
|
1416
|
-
const maxOutputsToUse = sortedOutputs.slice(0, MAX_TOKEN_OUTPUTS);
|
|
1417
|
-
const maxAmount = calculateAvailableTokenAmount(maxOutputsToUse);
|
|
1418
|
-
throw new ValidationError(
|
|
1419
|
-
`Cannot transfer more than ${MAX_TOKEN_OUTPUTS} TTXOs in a single transaction (${outputsToUse.length} selected). Maximum transferable amount is: ${maxAmount}`,
|
|
1420
|
-
{
|
|
1421
|
-
field: "outputsToUse",
|
|
1422
|
-
value: outputsToUse.length,
|
|
1423
|
-
expected: `Less than or equal to ${MAX_TOKEN_OUTPUTS}, with maximum transferable amount of ${maxAmount}`
|
|
1424
|
-
}
|
|
1425
|
-
);
|
|
1426
|
-
}
|
|
1427
|
-
const tokenOutputData = receiverOutputs.map((transfer) => {
|
|
1428
|
-
const receiverAddress = decodeSparkAddress(
|
|
1429
|
-
transfer.receiverSparkAddress,
|
|
1430
|
-
this.config.getNetworkType()
|
|
1431
|
-
);
|
|
1432
|
-
return {
|
|
1433
|
-
receiverSparkAddress: hexToBytes(receiverAddress.identityPublicKey),
|
|
1434
|
-
tokenPublicKey: hexToBytes(transfer.tokenPublicKey),
|
|
1435
|
-
tokenAmount: transfer.tokenAmount
|
|
1436
|
-
};
|
|
1437
|
-
});
|
|
1438
|
-
let tokenTransaction;
|
|
1439
|
-
if (this.config.getTokenTransactionVersion() === "V0") {
|
|
1440
|
-
tokenTransaction = await this.constructTransferTokenTransactionV0(
|
|
1441
|
-
outputsToUse,
|
|
1442
|
-
tokenOutputData
|
|
1443
|
-
);
|
|
1444
|
-
} else {
|
|
1445
|
-
tokenTransaction = await this.constructTransferTokenTransaction(
|
|
1446
|
-
outputsToUse,
|
|
1447
|
-
tokenOutputData
|
|
1448
|
-
);
|
|
1449
|
-
}
|
|
1450
|
-
const txId = await this.broadcastTokenTransaction(
|
|
1451
|
-
tokenTransaction,
|
|
1452
|
-
outputsToUse.map((output) => output.output.ownerPublicKey),
|
|
1453
|
-
outputsToUse.map((output) => output.output.revocationCommitment)
|
|
1454
|
-
);
|
|
1455
|
-
return txId;
|
|
1456
|
-
}
|
|
1457
|
-
async constructTransferTokenTransactionV0(selectedOutputs, tokenOutputData) {
|
|
1458
|
-
selectedOutputs.sort(
|
|
1459
|
-
(a, b) => a.previousTransactionVout - b.previousTransactionVout
|
|
1460
|
-
);
|
|
1461
|
-
const availableTokenAmount = calculateAvailableTokenAmount(selectedOutputs);
|
|
1462
|
-
const totalRequestedAmount = tokenOutputData.reduce(
|
|
1463
|
-
(sum, output) => sum + output.tokenAmount,
|
|
1464
|
-
0n
|
|
1465
|
-
);
|
|
1466
|
-
const tokenOutputs = tokenOutputData.map((output) => ({
|
|
1467
|
-
ownerPublicKey: output.receiverSparkAddress,
|
|
1468
|
-
tokenPublicKey: output.tokenPublicKey,
|
|
1469
|
-
tokenAmount: numberToBytesBE(output.tokenAmount, 16)
|
|
1470
|
-
}));
|
|
1471
|
-
if (availableTokenAmount > totalRequestedAmount) {
|
|
1472
|
-
const changeAmount = availableTokenAmount - totalRequestedAmount;
|
|
1473
|
-
const firstTokenPublicKey = tokenOutputData[0].tokenPublicKey;
|
|
1474
|
-
tokenOutputs.push({
|
|
1475
|
-
ownerPublicKey: await this.config.signer.getIdentityPublicKey(),
|
|
1476
|
-
tokenPublicKey: firstTokenPublicKey,
|
|
1477
|
-
tokenAmount: numberToBytesBE(changeAmount, 16)
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
|
-
return {
|
|
1481
|
-
network: this.config.getNetworkProto(),
|
|
1482
|
-
tokenInputs: {
|
|
1483
|
-
$case: "transferInput",
|
|
1484
|
-
transferInput: {
|
|
1485
|
-
outputsToSpend: selectedOutputs.map((output) => ({
|
|
1486
|
-
prevTokenTransactionHash: output.previousTransactionHash,
|
|
1487
|
-
prevTokenTransactionVout: output.previousTransactionVout
|
|
1488
|
-
}))
|
|
1489
|
-
}
|
|
1490
|
-
},
|
|
1491
|
-
tokenOutputs,
|
|
1492
|
-
sparkOperatorIdentityPublicKeys: this.collectOperatorIdentityPublicKeys()
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
async constructTransferTokenTransaction(selectedOutputs, tokenOutputData) {
|
|
1496
|
-
selectedOutputs.sort(
|
|
1497
|
-
(a, b) => a.previousTransactionVout - b.previousTransactionVout
|
|
1498
|
-
);
|
|
1499
|
-
const availableTokenAmount = calculateAvailableTokenAmount(selectedOutputs);
|
|
1500
|
-
const totalRequestedAmount = tokenOutputData.reduce(
|
|
1501
|
-
(sum, output) => sum + output.tokenAmount,
|
|
1502
|
-
0n
|
|
1503
|
-
);
|
|
1504
|
-
const tokenOutputs = tokenOutputData.map((output) => ({
|
|
1505
|
-
ownerPublicKey: output.receiverSparkAddress,
|
|
1506
|
-
tokenPublicKey: output.tokenPublicKey,
|
|
1507
|
-
tokenAmount: numberToBytesBE(output.tokenAmount, 16)
|
|
1508
|
-
}));
|
|
1509
|
-
if (availableTokenAmount > totalRequestedAmount) {
|
|
1510
|
-
const changeAmount = availableTokenAmount - totalRequestedAmount;
|
|
1511
|
-
const firstTokenPublicKey = tokenOutputData[0].tokenPublicKey;
|
|
1512
|
-
tokenOutputs.push({
|
|
1513
|
-
ownerPublicKey: await this.config.signer.getIdentityPublicKey(),
|
|
1514
|
-
tokenPublicKey: firstTokenPublicKey,
|
|
1515
|
-
tokenAmount: numberToBytesBE(changeAmount, 16)
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
1518
|
-
return {
|
|
1519
|
-
version: 1,
|
|
1520
|
-
network: this.config.getNetworkProto(),
|
|
1521
|
-
tokenInputs: {
|
|
1522
|
-
$case: "transferInput",
|
|
1523
|
-
transferInput: {
|
|
1524
|
-
outputsToSpend: selectedOutputs.map((output) => ({
|
|
1525
|
-
prevTokenTransactionHash: output.previousTransactionHash,
|
|
1526
|
-
prevTokenTransactionVout: output.previousTransactionVout
|
|
1527
|
-
}))
|
|
1528
|
-
}
|
|
1529
|
-
},
|
|
1530
|
-
tokenOutputs,
|
|
1531
|
-
sparkOperatorIdentityPublicKeys: this.collectOperatorIdentityPublicKeys(),
|
|
1532
|
-
expiryTime: void 0,
|
|
1533
|
-
clientCreatedTimestamp: /* @__PURE__ */ new Date()
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1536
|
-
collectOperatorIdentityPublicKeys() {
|
|
1537
|
-
const operatorKeys = [];
|
|
1538
|
-
for (const [_, operator] of Object.entries(
|
|
1539
|
-
this.config.getSigningOperators()
|
|
1540
|
-
)) {
|
|
1541
|
-
operatorKeys.push(hexToBytes(operator.identityPublicKey));
|
|
1542
|
-
}
|
|
1543
|
-
return operatorKeys;
|
|
1544
|
-
}
|
|
1545
|
-
async broadcastTokenTransaction(tokenTransaction, outputsToSpendSigningPublicKeys, outputsToSpendCommitments) {
|
|
1546
|
-
const signingOperators = this.config.getSigningOperators();
|
|
1547
|
-
if (!isTokenTransaction(tokenTransaction)) {
|
|
1548
|
-
return this.broadcastTokenTransactionV0(
|
|
1549
|
-
tokenTransaction,
|
|
1550
|
-
signingOperators,
|
|
1551
|
-
outputsToSpendSigningPublicKeys,
|
|
1552
|
-
outputsToSpendCommitments
|
|
1553
|
-
);
|
|
1554
|
-
} else {
|
|
1555
|
-
return this.broadcastTokenTransactionV1(
|
|
1556
|
-
tokenTransaction,
|
|
1557
|
-
signingOperators,
|
|
1558
|
-
outputsToSpendSigningPublicKeys,
|
|
1559
|
-
outputsToSpendCommitments
|
|
1560
|
-
);
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
async broadcastTokenTransactionV0(tokenTransaction, signingOperators, outputsToSpendSigningPublicKeys, outputsToSpendCommitments) {
|
|
1564
|
-
const { finalTokenTransaction, finalTokenTransactionHash, threshold } = await this.startTokenTransactionV0(
|
|
1565
|
-
tokenTransaction,
|
|
1566
|
-
signingOperators,
|
|
1567
|
-
outputsToSpendSigningPublicKeys,
|
|
1568
|
-
outputsToSpendCommitments
|
|
1569
|
-
);
|
|
1570
|
-
const { successfulSignatures } = await this.signTokenTransactionV0(
|
|
1571
|
-
finalTokenTransaction,
|
|
1572
|
-
finalTokenTransactionHash,
|
|
1573
|
-
signingOperators
|
|
1574
|
-
);
|
|
1575
|
-
if (finalTokenTransaction.tokenInputs.$case === "transferInput") {
|
|
1576
|
-
const outputsToSpend = finalTokenTransaction.tokenInputs.transferInput.outputsToSpend;
|
|
1577
|
-
const errors = [];
|
|
1578
|
-
const revocationSecrets = [];
|
|
1579
|
-
for (let outputIndex = 0; outputIndex < outputsToSpend.length; outputIndex++) {
|
|
1580
|
-
const outputKeyshares = successfulSignatures.map(({ identifier, response }) => ({
|
|
1581
|
-
operatorIndex: parseInt(identifier, 16),
|
|
1582
|
-
keyshare: response.revocationKeyshares[outputIndex]
|
|
1583
|
-
}));
|
|
1584
|
-
if (outputKeyshares.length < threshold) {
|
|
1585
|
-
errors.push(
|
|
1586
|
-
new ValidationError("Insufficient keyshares", {
|
|
1587
|
-
field: "outputKeyshares",
|
|
1588
|
-
value: outputKeyshares.length,
|
|
1589
|
-
expected: threshold,
|
|
1590
|
-
index: outputIndex
|
|
1591
|
-
})
|
|
1592
|
-
);
|
|
1593
|
-
}
|
|
1594
|
-
const seenIndices = /* @__PURE__ */ new Set();
|
|
1595
|
-
for (const { operatorIndex } of outputKeyshares) {
|
|
1596
|
-
if (seenIndices.has(operatorIndex)) {
|
|
1597
|
-
errors.push(
|
|
1598
|
-
new ValidationError("Duplicate operator index", {
|
|
1599
|
-
field: "outputKeyshares",
|
|
1600
|
-
value: operatorIndex,
|
|
1601
|
-
expected: "Unique operator index",
|
|
1602
|
-
index: outputIndex
|
|
1603
|
-
})
|
|
1604
|
-
);
|
|
1605
|
-
}
|
|
1606
|
-
seenIndices.add(operatorIndex);
|
|
1607
|
-
}
|
|
1608
|
-
const revocationSecret = recoverRevocationSecretFromKeyshares(
|
|
1609
|
-
outputKeyshares,
|
|
1610
|
-
threshold
|
|
1611
|
-
);
|
|
1612
|
-
const derivedRevocationCommitment = secp256k12.getPublicKey(
|
|
1613
|
-
revocationSecret,
|
|
1614
|
-
true
|
|
1615
|
-
);
|
|
1616
|
-
if (!outputsToSpendCommitments || !outputsToSpendCommitments[outputIndex] || !derivedRevocationCommitment.every(
|
|
1617
|
-
(byte, i) => byte === outputsToSpendCommitments[outputIndex][i]
|
|
1618
|
-
)) {
|
|
1619
|
-
errors.push(
|
|
1620
|
-
new InternalValidationError(
|
|
1621
|
-
"Revocation commitment verification failed",
|
|
1622
|
-
{
|
|
1623
|
-
field: "revocationCommitment",
|
|
1624
|
-
value: derivedRevocationCommitment,
|
|
1625
|
-
expected: bytesToHex(outputsToSpendCommitments[outputIndex]),
|
|
1626
|
-
outputIndex
|
|
1627
|
-
}
|
|
1628
|
-
)
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
revocationSecrets.push({
|
|
1632
|
-
inputIndex: outputIndex,
|
|
1633
|
-
revocationSecret
|
|
1634
|
-
});
|
|
1635
|
-
}
|
|
1636
|
-
if (errors.length > 0) {
|
|
1637
|
-
throw new ValidationError(
|
|
1638
|
-
"Multiple validation errors occurred across outputs",
|
|
1639
|
-
{
|
|
1640
|
-
field: "outputValidation",
|
|
1641
|
-
value: errors
|
|
1642
|
-
}
|
|
1643
|
-
);
|
|
1644
|
-
}
|
|
1645
|
-
await this.finalizeTokenTransaction(
|
|
1646
|
-
finalTokenTransaction,
|
|
1647
|
-
revocationSecrets,
|
|
1648
|
-
threshold
|
|
1649
|
-
);
|
|
1650
|
-
}
|
|
1651
|
-
return bytesToHex(finalTokenTransactionHash);
|
|
1652
|
-
}
|
|
1653
|
-
async broadcastTokenTransactionV1(tokenTransaction, signingOperators, outputsToSpendSigningPublicKeys, outputsToSpendCommitments) {
|
|
1654
|
-
const { finalTokenTransaction, finalTokenTransactionHash, threshold } = await this.startTokenTransaction(
|
|
1655
|
-
tokenTransaction,
|
|
1656
|
-
signingOperators,
|
|
1657
|
-
outputsToSpendSigningPublicKeys,
|
|
1658
|
-
outputsToSpendCommitments
|
|
1659
|
-
);
|
|
1660
|
-
await this.signTokenTransaction(
|
|
1661
|
-
finalTokenTransaction,
|
|
1662
|
-
finalTokenTransactionHash,
|
|
1663
|
-
signingOperators
|
|
1664
|
-
);
|
|
1665
|
-
return bytesToHex(finalTokenTransactionHash);
|
|
1666
|
-
}
|
|
1667
|
-
async startTokenTransactionV0(tokenTransaction, signingOperators, outputsToSpendSigningPublicKeys, outputsToSpendCommitments) {
|
|
1668
|
-
const sparkClient = await this.connectionManager.createSparkClient(
|
|
1669
|
-
this.config.getCoordinatorAddress()
|
|
1670
|
-
);
|
|
1671
|
-
const partialTokenTransactionHash = hashTokenTransactionV0(
|
|
1672
|
-
tokenTransaction,
|
|
1673
|
-
true
|
|
1674
|
-
);
|
|
1675
|
-
const ownerSignaturesWithIndex = [];
|
|
1676
|
-
if (tokenTransaction.tokenInputs.$case === "mintInput") {
|
|
1677
|
-
const issuerPublicKey = tokenTransaction.tokenInputs.mintInput.issuerPublicKey;
|
|
1678
|
-
if (!issuerPublicKey) {
|
|
1679
|
-
throw new ValidationError("Invalid mint input", {
|
|
1680
|
-
field: "issuerPublicKey",
|
|
1681
|
-
value: null,
|
|
1682
|
-
expected: "Non-null issuer public key"
|
|
1683
|
-
});
|
|
1684
|
-
}
|
|
1685
|
-
const ownerSignature = await this.signMessageWithKey(
|
|
1686
|
-
partialTokenTransactionHash,
|
|
1687
|
-
issuerPublicKey
|
|
1688
|
-
);
|
|
1689
|
-
ownerSignaturesWithIndex.push({
|
|
1690
|
-
signature: ownerSignature,
|
|
1691
|
-
inputIndex: 0
|
|
1692
|
-
});
|
|
1693
|
-
} else if (tokenTransaction.tokenInputs.$case === "transferInput") {
|
|
1694
|
-
if (!outputsToSpendSigningPublicKeys || !outputsToSpendCommitments) {
|
|
1695
|
-
throw new ValidationError("Invalid transfer input", {
|
|
1696
|
-
field: "outputsToSpend",
|
|
1697
|
-
value: {
|
|
1698
|
-
signingPublicKeys: outputsToSpendSigningPublicKeys,
|
|
1699
|
-
revocationPublicKeys: outputsToSpendCommitments
|
|
1700
|
-
},
|
|
1701
|
-
expected: "Non-null signing and revocation public keys"
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
for (const [i, key] of outputsToSpendSigningPublicKeys.entries()) {
|
|
1705
|
-
if (!key) {
|
|
1706
|
-
throw new ValidationError("Invalid signing key", {
|
|
1707
|
-
field: "outputsToSpendSigningPublicKeys",
|
|
1708
|
-
value: i,
|
|
1709
|
-
expected: "Non-null signing key"
|
|
1710
|
-
});
|
|
1711
|
-
}
|
|
1712
|
-
const ownerSignature = await this.signMessageWithKey(
|
|
1713
|
-
partialTokenTransactionHash,
|
|
1714
|
-
key
|
|
1715
|
-
);
|
|
1716
|
-
ownerSignaturesWithIndex.push({
|
|
1717
|
-
signature: ownerSignature,
|
|
1718
|
-
inputIndex: i
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
const startResponse = await sparkClient.start_token_transaction(
|
|
1723
|
-
{
|
|
1724
|
-
identityPublicKey: await this.config.signer.getIdentityPublicKey(),
|
|
1725
|
-
partialTokenTransaction: tokenTransaction,
|
|
1726
|
-
tokenTransactionSignatures: {
|
|
1727
|
-
ownerSignatures: ownerSignaturesWithIndex
|
|
1728
|
-
}
|
|
1729
|
-
},
|
|
1730
|
-
{
|
|
1731
|
-
retry: true,
|
|
1732
|
-
retryableStatuses: ["UNKNOWN", "UNAVAILABLE", "CANCELLED", "INTERNAL"],
|
|
1733
|
-
retryMaxAttempts: 3
|
|
1734
|
-
}
|
|
1735
|
-
);
|
|
1736
|
-
if (!startResponse.finalTokenTransaction) {
|
|
1737
|
-
throw new Error("Final token transaction missing in start response");
|
|
1738
|
-
}
|
|
1739
|
-
if (!startResponse.keyshareInfo) {
|
|
1740
|
-
throw new Error("Keyshare info missing in start response");
|
|
1741
|
-
}
|
|
1742
|
-
validateTokenTransactionV0(
|
|
1743
|
-
startResponse.finalTokenTransaction,
|
|
1744
|
-
tokenTransaction,
|
|
1745
|
-
signingOperators,
|
|
1746
|
-
startResponse.keyshareInfo,
|
|
1747
|
-
this.config.getExpectedWithdrawBondSats(),
|
|
1748
|
-
this.config.getExpectedWithdrawRelativeBlockLocktime(),
|
|
1749
|
-
this.config.getThreshold()
|
|
1750
|
-
);
|
|
1751
|
-
const finalTokenTransaction = startResponse.finalTokenTransaction;
|
|
1752
|
-
const finalTokenTransactionHash = hashTokenTransactionV0(
|
|
1753
|
-
finalTokenTransaction,
|
|
1754
|
-
false
|
|
1755
|
-
);
|
|
1756
|
-
return {
|
|
1757
|
-
finalTokenTransaction,
|
|
1758
|
-
finalTokenTransactionHash,
|
|
1759
|
-
threshold: startResponse.keyshareInfo.threshold
|
|
1760
|
-
};
|
|
1761
|
-
}
|
|
1762
|
-
async startTokenTransaction(tokenTransaction, signingOperators, outputsToSpendSigningPublicKeys, outputsToSpendCommitments) {
|
|
1763
|
-
const sparkClient = await this.connectionManager.createSparkTokenClient(
|
|
1764
|
-
this.config.getCoordinatorAddress()
|
|
1765
|
-
);
|
|
1766
|
-
const partialTokenTransactionHash = hashTokenTransaction(
|
|
1767
|
-
tokenTransaction,
|
|
1768
|
-
true
|
|
1769
|
-
);
|
|
1770
|
-
const ownerSignaturesWithIndex = [];
|
|
1771
|
-
if (tokenTransaction.tokenInputs.$case === "mintInput") {
|
|
1772
|
-
const issuerPublicKey = tokenTransaction.tokenInputs.mintInput.issuerPublicKey;
|
|
1773
|
-
if (!issuerPublicKey) {
|
|
1774
|
-
throw new ValidationError("Invalid mint input", {
|
|
1775
|
-
field: "issuerPublicKey",
|
|
1776
|
-
value: null,
|
|
1777
|
-
expected: "Non-null issuer public key"
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
1780
|
-
const ownerSignature = await this.signMessageWithKey(
|
|
1781
|
-
partialTokenTransactionHash,
|
|
1782
|
-
issuerPublicKey
|
|
1783
|
-
);
|
|
1784
|
-
ownerSignaturesWithIndex.push({
|
|
1785
|
-
signature: ownerSignature,
|
|
1786
|
-
inputIndex: 0
|
|
1787
|
-
});
|
|
1788
|
-
} else if (tokenTransaction.tokenInputs.$case === "transferInput") {
|
|
1789
|
-
if (!outputsToSpendSigningPublicKeys || !outputsToSpendCommitments) {
|
|
1790
|
-
throw new ValidationError("Invalid transfer input", {
|
|
1791
|
-
field: "outputsToSpend",
|
|
1792
|
-
value: {
|
|
1793
|
-
signingPublicKeys: outputsToSpendSigningPublicKeys,
|
|
1794
|
-
revocationPublicKeys: outputsToSpendCommitments
|
|
1795
|
-
},
|
|
1796
|
-
expected: "Non-null signing and revocation public keys"
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
for (const [i, key] of outputsToSpendSigningPublicKeys.entries()) {
|
|
1800
|
-
if (!key) {
|
|
1801
|
-
throw new ValidationError("Invalid signing key", {
|
|
1802
|
-
field: "outputsToSpendSigningPublicKeys",
|
|
1803
|
-
value: i,
|
|
1804
|
-
expected: "Non-null signing key"
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
const ownerSignature = await this.signMessageWithKey(
|
|
1808
|
-
partialTokenTransactionHash,
|
|
1809
|
-
key
|
|
1810
|
-
);
|
|
1811
|
-
ownerSignaturesWithIndex.push({
|
|
1812
|
-
signature: ownerSignature,
|
|
1813
|
-
inputIndex: i
|
|
1814
|
-
});
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
const startResponse = await sparkClient.start_transaction(
|
|
1818
|
-
{
|
|
1819
|
-
identityPublicKey: await this.config.signer.getIdentityPublicKey(),
|
|
1820
|
-
partialTokenTransaction: tokenTransaction,
|
|
1821
|
-
validityDurationSeconds: await this.config.getTokenValidityDurationSeconds(),
|
|
1822
|
-
partialTokenTransactionOwnerSignatures: ownerSignaturesWithIndex
|
|
1823
|
-
},
|
|
1824
|
-
{
|
|
1825
|
-
retry: true,
|
|
1826
|
-
retryableStatuses: ["UNKNOWN", "UNAVAILABLE", "CANCELLED", "INTERNAL"],
|
|
1827
|
-
retryMaxAttempts: 3
|
|
1828
|
-
}
|
|
1829
|
-
);
|
|
1830
|
-
if (!startResponse.finalTokenTransaction) {
|
|
1831
|
-
throw new Error("Final token transaction missing in start response");
|
|
1832
|
-
}
|
|
1833
|
-
if (!startResponse.keyshareInfo) {
|
|
1834
|
-
throw new Error("Keyshare info missing in start response");
|
|
1835
|
-
}
|
|
1836
|
-
validateTokenTransaction(
|
|
1837
|
-
startResponse.finalTokenTransaction,
|
|
1838
|
-
tokenTransaction,
|
|
1839
|
-
signingOperators,
|
|
1840
|
-
startResponse.keyshareInfo,
|
|
1841
|
-
this.config.getExpectedWithdrawBondSats(),
|
|
1842
|
-
this.config.getExpectedWithdrawRelativeBlockLocktime(),
|
|
1843
|
-
this.config.getThreshold()
|
|
1844
|
-
);
|
|
1845
|
-
const finalTokenTransaction = startResponse.finalTokenTransaction;
|
|
1846
|
-
const finalTokenTransactionHash = hashTokenTransaction(
|
|
1847
|
-
finalTokenTransaction,
|
|
1848
|
-
false
|
|
1849
|
-
);
|
|
1850
|
-
return {
|
|
1851
|
-
finalTokenTransaction,
|
|
1852
|
-
finalTokenTransactionHash,
|
|
1853
|
-
threshold: startResponse.keyshareInfo.threshold
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
async signTokenTransactionV0(finalTokenTransaction, finalTokenTransactionHash, signingOperators) {
|
|
1857
|
-
const soSignatures = await Promise.allSettled(
|
|
1858
|
-
Object.entries(signingOperators).map(
|
|
1859
|
-
async ([identifier, operator], index) => {
|
|
1860
|
-
const internalSparkClient = await this.connectionManager.createSparkClient(operator.address);
|
|
1861
|
-
const identityPublicKey = await this.config.signer.getIdentityPublicKey();
|
|
1862
|
-
const payload = {
|
|
1863
|
-
finalTokenTransactionHash,
|
|
1864
|
-
operatorIdentityPublicKey: hexToBytes(operator.identityPublicKey)
|
|
1865
|
-
};
|
|
1866
|
-
const payloadHash = await hashOperatorSpecificTokenTransactionSignablePayload(payload);
|
|
1867
|
-
let operatorSpecificSignatures = [];
|
|
1868
|
-
if (finalTokenTransaction.tokenInputs.$case === "mintInput") {
|
|
1869
|
-
const issuerPublicKey = finalTokenTransaction.tokenInputs.mintInput.issuerPublicKey;
|
|
1870
|
-
if (!issuerPublicKey) {
|
|
1871
|
-
throw new ValidationError("Invalid mint input", {
|
|
1872
|
-
field: "issuerPublicKey",
|
|
1873
|
-
value: null,
|
|
1874
|
-
expected: "Non-null issuer public key"
|
|
1875
|
-
});
|
|
1876
|
-
}
|
|
1877
|
-
const ownerSignature = await this.signMessageWithKey(
|
|
1878
|
-
payloadHash,
|
|
1879
|
-
issuerPublicKey
|
|
1880
|
-
);
|
|
1881
|
-
operatorSpecificSignatures.push({
|
|
1882
|
-
ownerSignature: {
|
|
1883
|
-
signature: ownerSignature,
|
|
1884
|
-
inputIndex: 0
|
|
1885
|
-
},
|
|
1886
|
-
payload
|
|
1887
|
-
});
|
|
1888
|
-
}
|
|
1889
|
-
if (finalTokenTransaction.tokenInputs.$case === "transferInput") {
|
|
1890
|
-
const transferInput = finalTokenTransaction.tokenInputs.transferInput;
|
|
1891
|
-
for (let i = 0; i < transferInput.outputsToSpend.length; i++) {
|
|
1892
|
-
let ownerSignature;
|
|
1893
|
-
if (this.config.getTokenSignatures() === "SCHNORR") {
|
|
1894
|
-
ownerSignature = await this.config.signer.signSchnorrWithIdentityKey(
|
|
1895
|
-
payloadHash
|
|
1896
|
-
);
|
|
1897
|
-
} else {
|
|
1898
|
-
ownerSignature = await this.config.signer.signMessageWithIdentityKey(
|
|
1899
|
-
payloadHash
|
|
1900
|
-
);
|
|
1901
|
-
}
|
|
1902
|
-
operatorSpecificSignatures.push({
|
|
1903
|
-
ownerSignature: {
|
|
1904
|
-
signature: ownerSignature,
|
|
1905
|
-
inputIndex: i
|
|
1906
|
-
},
|
|
1907
|
-
payload
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
try {
|
|
1912
|
-
const response = await internalSparkClient.sign_token_transaction(
|
|
1913
|
-
{
|
|
1914
|
-
finalTokenTransaction,
|
|
1915
|
-
operatorSpecificSignatures,
|
|
1916
|
-
identityPublicKey
|
|
1917
|
-
},
|
|
1918
|
-
{
|
|
1919
|
-
retry: true,
|
|
1920
|
-
retryableStatuses: [
|
|
1921
|
-
"UNKNOWN",
|
|
1922
|
-
"UNAVAILABLE",
|
|
1923
|
-
"CANCELLED",
|
|
1924
|
-
"INTERNAL"
|
|
1925
|
-
],
|
|
1926
|
-
retryMaxAttempts: 3
|
|
1927
|
-
}
|
|
1928
|
-
);
|
|
1929
|
-
return {
|
|
1930
|
-
index,
|
|
1931
|
-
identifier,
|
|
1932
|
-
response
|
|
1933
|
-
};
|
|
1934
|
-
} catch (error) {
|
|
1935
|
-
throw new NetworkError(
|
|
1936
|
-
"Failed to sign token transaction",
|
|
1937
|
-
{
|
|
1938
|
-
operation: "sign_token_transaction",
|
|
1939
|
-
errorCount: 1,
|
|
1940
|
-
errors: error instanceof Error ? error.message : String(error)
|
|
1941
|
-
},
|
|
1942
|
-
error
|
|
1943
|
-
);
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
)
|
|
1947
|
-
);
|
|
1948
|
-
const successfulSignatures = collectResponses(soSignatures);
|
|
1949
|
-
return {
|
|
1950
|
-
successfulSignatures
|
|
1951
|
-
};
|
|
1952
|
-
}
|
|
1953
|
-
async signTokenTransaction(finalTokenTransaction, finalTokenTransactionHash, signingOperators) {
|
|
1954
|
-
const coordinatorClient = await this.connectionManager.createSparkTokenClient(
|
|
1955
|
-
this.config.getCoordinatorAddress()
|
|
1956
|
-
);
|
|
1957
|
-
const inputTtxoSignaturesPerOperator = await this.createSignaturesForOperators(
|
|
1958
|
-
finalTokenTransaction,
|
|
1959
|
-
finalTokenTransactionHash,
|
|
1960
|
-
signingOperators
|
|
1961
|
-
);
|
|
1962
|
-
try {
|
|
1963
|
-
await coordinatorClient.commit_transaction(
|
|
1964
|
-
{
|
|
1965
|
-
finalTokenTransaction,
|
|
1966
|
-
finalTokenTransactionHash,
|
|
1967
|
-
inputTtxoSignaturesPerOperator,
|
|
1968
|
-
ownerIdentityPublicKey: await this.config.signer.getIdentityPublicKey()
|
|
1969
|
-
},
|
|
1970
|
-
{
|
|
1971
|
-
retry: true,
|
|
1972
|
-
retryableStatuses: [
|
|
1973
|
-
"UNKNOWN",
|
|
1974
|
-
"UNAVAILABLE",
|
|
1975
|
-
"CANCELLED",
|
|
1976
|
-
"INTERNAL"
|
|
1977
|
-
],
|
|
1978
|
-
retryMaxAttempts: 3
|
|
1979
|
-
}
|
|
1980
|
-
);
|
|
1981
|
-
} catch (error) {
|
|
1982
|
-
throw new NetworkError(
|
|
1983
|
-
"Failed to sign token transaction",
|
|
1984
|
-
{
|
|
1985
|
-
operation: "sign_token_transaction",
|
|
1986
|
-
errorCount: 1,
|
|
1987
|
-
errors: error instanceof Error ? error.message : String(error)
|
|
1988
|
-
},
|
|
1989
|
-
error
|
|
1990
|
-
);
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
async fetchOwnedTokenOutputs(params) {
|
|
1994
|
-
if (this.config.getTokenTransactionVersion() === "V0") {
|
|
1995
|
-
return this.fetchOwnedTokenOutputsV0(params);
|
|
1996
|
-
} else {
|
|
1997
|
-
return this.fetchOwnedTokenOutputsV1(params);
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
async queryTokenTransactions(params) {
|
|
2001
|
-
if (this.config.getTokenTransactionVersion() === "V0") {
|
|
2002
|
-
return this.queryTokenTransactionsV0(params);
|
|
2003
|
-
} else {
|
|
2004
|
-
return this.queryTokenTransactionsV1(params);
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
async fetchOwnedTokenOutputsV0(params) {
|
|
2008
|
-
const {
|
|
2009
|
-
ownerPublicKeys,
|
|
2010
|
-
issuerPublicKeys: tokenPublicKeys = [],
|
|
2011
|
-
tokenIdentifiers = []
|
|
2012
|
-
} = params;
|
|
2013
|
-
const sparkClient = await this.connectionManager.createSparkClient(
|
|
2014
|
-
this.config.getCoordinatorAddress()
|
|
2015
|
-
);
|
|
2016
|
-
try {
|
|
2017
|
-
const result = await sparkClient.query_token_outputs({
|
|
2018
|
-
ownerPublicKeys,
|
|
2019
|
-
tokenPublicKeys,
|
|
2020
|
-
tokenIdentifiers,
|
|
2021
|
-
network: this.config.getNetworkProto()
|
|
2022
|
-
});
|
|
2023
|
-
return result.outputsWithPreviousTransactionData;
|
|
2024
|
-
} catch (error) {
|
|
2025
|
-
throw new NetworkError(
|
|
2026
|
-
"Failed to fetch owned token outputs",
|
|
2027
|
-
{
|
|
2028
|
-
operation: "spark.query_token_outputs",
|
|
2029
|
-
errorCount: 1,
|
|
2030
|
-
errors: error instanceof Error ? error.message : String(error)
|
|
2031
|
-
},
|
|
2032
|
-
error
|
|
2033
|
-
);
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
async fetchOwnedTokenOutputsV1(params) {
|
|
2037
|
-
const {
|
|
2038
|
-
ownerPublicKeys,
|
|
2039
|
-
issuerPublicKeys = [],
|
|
2040
|
-
tokenIdentifiers = []
|
|
2041
|
-
} = params;
|
|
2042
|
-
const tokenClient = await this.connectionManager.createSparkTokenClient(
|
|
2043
|
-
this.config.getCoordinatorAddress()
|
|
2044
|
-
);
|
|
2045
|
-
try {
|
|
2046
|
-
const result = await tokenClient.query_token_outputs({
|
|
2047
|
-
ownerPublicKeys,
|
|
2048
|
-
issuerPublicKeys,
|
|
2049
|
-
tokenIdentifiers,
|
|
2050
|
-
network: this.config.getNetworkProto()
|
|
2051
|
-
});
|
|
2052
|
-
return result.outputsWithPreviousTransactionData;
|
|
2053
|
-
} catch (error) {
|
|
2054
|
-
throw new NetworkError(
|
|
2055
|
-
"Failed to fetch owned token outputs",
|
|
2056
|
-
{
|
|
2057
|
-
operation: "spark_token.query_token_outputs",
|
|
2058
|
-
errorCount: 1,
|
|
2059
|
-
errors: error instanceof Error ? error.message : String(error)
|
|
2060
|
-
},
|
|
2061
|
-
error
|
|
2062
|
-
);
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
async queryTokenTransactionsV0(params) {
|
|
2066
|
-
const {
|
|
2067
|
-
ownerPublicKeys,
|
|
2068
|
-
issuerPublicKeys,
|
|
2069
|
-
tokenTransactionHashes,
|
|
2070
|
-
tokenIdentifiers,
|
|
2071
|
-
outputIds
|
|
2072
|
-
} = params;
|
|
2073
|
-
const sparkClient = await this.connectionManager.createSparkClient(
|
|
2074
|
-
this.config.getCoordinatorAddress()
|
|
2075
|
-
);
|
|
2076
|
-
let queryParams = {
|
|
2077
|
-
tokenPublicKeys: issuerPublicKeys?.map(hexToBytes),
|
|
2078
|
-
ownerPublicKeys: ownerPublicKeys?.map(hexToBytes),
|
|
2079
|
-
tokenIdentifiers: tokenIdentifiers?.map(hexToBytes),
|
|
2080
|
-
tokenTransactionHashes: tokenTransactionHashes?.map(hexToBytes),
|
|
2081
|
-
outputIds: outputIds || [],
|
|
2082
|
-
limit: 100,
|
|
2083
|
-
offset: 0
|
|
2084
|
-
};
|
|
2085
|
-
try {
|
|
2086
|
-
const response = await sparkClient.query_token_transactions(queryParams);
|
|
2087
|
-
return response.tokenTransactionsWithStatus.map((tx) => {
|
|
2088
|
-
const v1TokenTransaction = {
|
|
2089
|
-
version: 1,
|
|
2090
|
-
network: tx.tokenTransaction.network,
|
|
2091
|
-
tokenInputs: tx.tokenTransaction.tokenInputs,
|
|
2092
|
-
tokenOutputs: tx.tokenTransaction.tokenOutputs,
|
|
2093
|
-
sparkOperatorIdentityPublicKeys: tx.tokenTransaction.sparkOperatorIdentityPublicKeys,
|
|
2094
|
-
expiryTime: void 0,
|
|
2095
|
-
// V0 doesn't have expiry time
|
|
2096
|
-
clientCreatedTimestamp: tx.tokenTransaction?.tokenInputs?.$case === "mintInput" ? new Date(
|
|
2097
|
-
tx.tokenTransaction.tokenInputs.mintInput.issuerProvidedTimestamp * 1e3
|
|
2098
|
-
) : /* @__PURE__ */ new Date()
|
|
2099
|
-
};
|
|
2100
|
-
return {
|
|
2101
|
-
tokenTransaction: v1TokenTransaction,
|
|
2102
|
-
status: tx.status,
|
|
2103
|
-
confirmationMetadata: tx.confirmationMetadata
|
|
2104
|
-
};
|
|
2105
|
-
});
|
|
2106
|
-
} catch (error) {
|
|
2107
|
-
throw new NetworkError(
|
|
2108
|
-
"Failed to query token transactions",
|
|
2109
|
-
{
|
|
2110
|
-
operation: "spark.query_token_transactions",
|
|
2111
|
-
errorCount: 1,
|
|
2112
|
-
errors: error instanceof Error ? error.message : String(error)
|
|
2113
|
-
},
|
|
2114
|
-
error
|
|
2115
|
-
);
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
async queryTokenTransactionsV1(params) {
|
|
2119
|
-
const {
|
|
2120
|
-
ownerPublicKeys,
|
|
2121
|
-
issuerPublicKeys,
|
|
2122
|
-
tokenTransactionHashes,
|
|
2123
|
-
tokenIdentifiers,
|
|
2124
|
-
outputIds
|
|
2125
|
-
} = params;
|
|
2126
|
-
const tokenClient = await this.connectionManager.createSparkTokenClient(
|
|
2127
|
-
this.config.getCoordinatorAddress()
|
|
2128
|
-
);
|
|
2129
|
-
let queryParams = {
|
|
2130
|
-
issuerPublicKeys: issuerPublicKeys?.map(hexToBytes),
|
|
2131
|
-
ownerPublicKeys: ownerPublicKeys?.map(hexToBytes),
|
|
2132
|
-
tokenIdentifiers: tokenIdentifiers?.map(hexToBytes),
|
|
2133
|
-
tokenTransactionHashes: tokenTransactionHashes?.map(hexToBytes),
|
|
2134
|
-
outputIds: outputIds || [],
|
|
2135
|
-
limit: 100,
|
|
2136
|
-
offset: 0
|
|
2137
|
-
};
|
|
2138
|
-
try {
|
|
2139
|
-
const response = await tokenClient.query_token_transactions(queryParams);
|
|
2140
|
-
return response.tokenTransactionsWithStatus;
|
|
2141
|
-
} catch (error) {
|
|
2142
|
-
throw new NetworkError(
|
|
2143
|
-
"Failed to query token transactions",
|
|
2144
|
-
{
|
|
2145
|
-
operation: "spark_token.query_token_transactions",
|
|
2146
|
-
errorCount: 1,
|
|
2147
|
-
errors: error instanceof Error ? error.message : String(error)
|
|
2148
|
-
},
|
|
2149
|
-
error
|
|
2150
|
-
);
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
async syncTokenOutputs(tokenOutputs) {
|
|
2154
|
-
const unsortedTokenOutputs = await this.fetchOwnedTokenOutputs({
|
|
2155
|
-
ownerPublicKeys: await this.config.signer.getTrackedPublicKeys()
|
|
2156
|
-
});
|
|
2157
|
-
unsortedTokenOutputs.forEach((output) => {
|
|
2158
|
-
const tokenKey = bytesToHex(output.output.tokenPublicKey);
|
|
2159
|
-
const index = output.previousTransactionVout;
|
|
2160
|
-
tokenOutputs.set(tokenKey, [
|
|
2161
|
-
{ ...output, previousTransactionVout: index }
|
|
2162
|
-
]);
|
|
2163
|
-
});
|
|
2164
|
-
}
|
|
2165
|
-
selectTokenOutputs(tokenOutputs, tokenAmount, strategy) {
|
|
2166
|
-
if (calculateAvailableTokenAmount(tokenOutputs) < tokenAmount) {
|
|
2167
|
-
throw new ValidationError("Insufficient token amount", {
|
|
2168
|
-
field: "tokenAmount",
|
|
2169
|
-
value: calculateAvailableTokenAmount(tokenOutputs),
|
|
2170
|
-
expected: tokenAmount
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
const exactMatch = tokenOutputs.find(
|
|
2174
|
-
(item) => bytesToNumberBE(item.output.tokenAmount) === tokenAmount
|
|
2175
|
-
);
|
|
2176
|
-
if (exactMatch) {
|
|
2177
|
-
return [exactMatch];
|
|
2178
|
-
}
|
|
2179
|
-
this.sortTokenOutputsByStrategy(tokenOutputs, strategy);
|
|
2180
|
-
let remainingAmount = tokenAmount;
|
|
2181
|
-
const selectedOutputs = [];
|
|
2182
|
-
for (const outputWithPreviousTransactionData of tokenOutputs) {
|
|
2183
|
-
if (remainingAmount <= 0n) break;
|
|
2184
|
-
selectedOutputs.push(outputWithPreviousTransactionData);
|
|
2185
|
-
remainingAmount -= bytesToNumberBE(
|
|
2186
|
-
outputWithPreviousTransactionData.output.tokenAmount
|
|
2187
|
-
);
|
|
2188
|
-
}
|
|
2189
|
-
if (remainingAmount > 0n) {
|
|
2190
|
-
throw new ValidationError("Insufficient funds", {
|
|
2191
|
-
field: "remainingAmount",
|
|
2192
|
-
value: remainingAmount
|
|
2193
|
-
});
|
|
2194
|
-
}
|
|
2195
|
-
return selectedOutputs;
|
|
2196
|
-
}
|
|
2197
|
-
sortTokenOutputsByStrategy(tokenOutputs, strategy) {
|
|
2198
|
-
if (strategy === "SMALL_FIRST") {
|
|
2199
|
-
tokenOutputs.sort((a, b) => {
|
|
2200
|
-
return Number(
|
|
2201
|
-
bytesToNumberBE(a.output.tokenAmount) - bytesToNumberBE(b.output.tokenAmount)
|
|
2202
|
-
);
|
|
2203
|
-
});
|
|
2204
|
-
} else {
|
|
2205
|
-
tokenOutputs.sort((a, b) => {
|
|
2206
|
-
return Number(
|
|
2207
|
-
bytesToNumberBE(b.output.tokenAmount) - bytesToNumberBE(a.output.tokenAmount)
|
|
2208
|
-
);
|
|
2209
|
-
});
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
// Helper function for deciding if the signer public key is the identity public key
|
|
2213
|
-
async signMessageWithKey(message, publicKey) {
|
|
2214
|
-
const tokenSignatures = this.config.getTokenSignatures();
|
|
2215
|
-
if (bytesToHex(publicKey) === bytesToHex(await this.config.signer.getIdentityPublicKey())) {
|
|
2216
|
-
if (tokenSignatures === "SCHNORR") {
|
|
2217
|
-
return await this.config.signer.signSchnorrWithIdentityKey(message);
|
|
2218
|
-
} else {
|
|
2219
|
-
return await this.config.signer.signMessageWithIdentityKey(message);
|
|
2220
|
-
}
|
|
2221
|
-
} else {
|
|
2222
|
-
if (tokenSignatures === "SCHNORR") {
|
|
2223
|
-
return await this.config.signer.signSchnorr(message, publicKey);
|
|
2224
|
-
} else {
|
|
2225
|
-
return await this.config.signer.signMessageWithPublicKey(
|
|
2226
|
-
message,
|
|
2227
|
-
publicKey
|
|
2228
|
-
);
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
async finalizeTokenTransaction(finalTokenTransaction, revocationSecrets, threshold) {
|
|
2233
|
-
const signingOperators = this.config.getSigningOperators();
|
|
2234
|
-
const soResponses = await Promise.allSettled(
|
|
2235
|
-
Object.entries(signingOperators).map(async ([identifier, operator]) => {
|
|
2236
|
-
const internalSparkClient = await this.connectionManager.createSparkClient(operator.address);
|
|
2237
|
-
const identityPublicKey = await this.config.signer.getIdentityPublicKey();
|
|
2238
|
-
try {
|
|
2239
|
-
const response = await internalSparkClient.finalize_token_transaction(
|
|
2240
|
-
{
|
|
2241
|
-
finalTokenTransaction,
|
|
2242
|
-
revocationSecrets,
|
|
2243
|
-
identityPublicKey
|
|
2244
|
-
},
|
|
2245
|
-
{
|
|
2246
|
-
retry: true,
|
|
2247
|
-
retryableStatuses: [
|
|
2248
|
-
"UNKNOWN",
|
|
2249
|
-
"UNAVAILABLE",
|
|
2250
|
-
"CANCELLED",
|
|
2251
|
-
"INTERNAL"
|
|
2252
|
-
],
|
|
2253
|
-
retryMaxAttempts: 3
|
|
2254
|
-
}
|
|
2255
|
-
);
|
|
2256
|
-
return {
|
|
2257
|
-
identifier,
|
|
2258
|
-
response
|
|
2259
|
-
};
|
|
2260
|
-
} catch (error) {
|
|
2261
|
-
throw new NetworkError(
|
|
2262
|
-
"Failed to finalize token transaction",
|
|
2263
|
-
{
|
|
2264
|
-
operation: "finalize_token_transaction",
|
|
2265
|
-
errorCount: 1,
|
|
2266
|
-
errors: error instanceof Error ? error.message : String(error)
|
|
2267
|
-
},
|
|
2268
|
-
error
|
|
2269
|
-
);
|
|
2270
|
-
}
|
|
2271
|
-
})
|
|
2272
|
-
);
|
|
2273
|
-
collectResponses(soResponses);
|
|
2274
|
-
return finalTokenTransaction;
|
|
2275
|
-
}
|
|
2276
|
-
async createSignaturesForOperators(finalTokenTransaction, finalTokenTransactionHash, signingOperators) {
|
|
2277
|
-
const inputTtxoSignaturesPerOperator = [];
|
|
2278
|
-
for (const [_, operator] of Object.entries(signingOperators)) {
|
|
2279
|
-
let ttxoSignatures = [];
|
|
2280
|
-
if (finalTokenTransaction.tokenInputs.$case === "mintInput") {
|
|
2281
|
-
const issuerPublicKey = finalTokenTransaction.tokenInputs.mintInput.issuerPublicKey;
|
|
2282
|
-
if (!issuerPublicKey) {
|
|
2283
|
-
throw new ValidationError("Invalid mint input", {
|
|
2284
|
-
field: "issuerPublicKey",
|
|
2285
|
-
value: null,
|
|
2286
|
-
expected: "Non-null issuer public key"
|
|
2287
|
-
});
|
|
2288
|
-
}
|
|
2289
|
-
const payload = {
|
|
2290
|
-
finalTokenTransactionHash,
|
|
2291
|
-
operatorIdentityPublicKey: hexToBytes(operator.identityPublicKey)
|
|
2292
|
-
};
|
|
2293
|
-
const payloadHash = await hashOperatorSpecificTokenTransactionSignablePayload(payload);
|
|
2294
|
-
const ownerSignature = await this.signMessageWithKey(
|
|
2295
|
-
payloadHash,
|
|
2296
|
-
issuerPublicKey
|
|
2297
|
-
);
|
|
2298
|
-
ttxoSignatures.push({
|
|
2299
|
-
signature: ownerSignature,
|
|
2300
|
-
inputIndex: 0
|
|
2301
|
-
});
|
|
2302
|
-
} else if (finalTokenTransaction.tokenInputs.$case === "transferInput") {
|
|
2303
|
-
const transferInput = finalTokenTransaction.tokenInputs.transferInput;
|
|
2304
|
-
for (let i = 0; i < transferInput.outputsToSpend.length; i++) {
|
|
2305
|
-
const payload = {
|
|
2306
|
-
finalTokenTransactionHash,
|
|
2307
|
-
operatorIdentityPublicKey: hexToBytes(operator.identityPublicKey)
|
|
2308
|
-
};
|
|
2309
|
-
const payloadHash = await hashOperatorSpecificTokenTransactionSignablePayload(payload);
|
|
2310
|
-
let ownerSignature;
|
|
2311
|
-
if (this.config.getTokenSignatures() === "SCHNORR") {
|
|
2312
|
-
ownerSignature = await this.config.signer.signSchnorrWithIdentityKey(payloadHash);
|
|
2313
|
-
} else {
|
|
2314
|
-
ownerSignature = await this.config.signer.signMessageWithIdentityKey(payloadHash);
|
|
2315
|
-
}
|
|
2316
|
-
ttxoSignatures.push({
|
|
2317
|
-
signature: ownerSignature,
|
|
2318
|
-
inputIndex: i
|
|
2319
|
-
});
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
inputTtxoSignaturesPerOperator.push({
|
|
2323
|
-
ttxoSignatures,
|
|
2324
|
-
operatorIdentityPublicKey: hexToBytes(operator.identityPublicKey)
|
|
2325
|
-
});
|
|
2326
|
-
}
|
|
2327
|
-
return inputTtxoSignaturesPerOperator;
|
|
2328
|
-
}
|
|
2329
|
-
};
|
|
2330
|
-
function isTokenTransaction(tokenTransaction) {
|
|
2331
|
-
return "version" in tokenTransaction && "expiryTime" in tokenTransaction;
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
export {
|
|
2335
|
-
TokenTransactionService
|
|
2336
|
-
};
|