@buildonspark/spark-sdk 0.0.14 → 0.0.16
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/dist/proto/spark.d.ts +111 -3
- package/dist/proto/spark.js +994 -43
- package/dist/proto/spark.js.map +1 -1
- package/dist/services/lightning.js +1 -0
- package/dist/services/lightning.js.map +1 -1
- package/dist/services/transfer.js +2 -0
- package/dist/services/transfer.js.map +1 -1
- package/dist/services/wallet-config.d.ts +1 -0
- package/dist/services/wallet-config.js +1 -0
- package/dist/services/wallet-config.js.map +1 -1
- package/dist/signer/signer.js +1 -1
- package/dist/signer/signer.js.map +1 -1
- package/dist/spark-sdk.d.ts +1 -1
- package/dist/spark-sdk.js +8 -4
- package/dist/spark-sdk.js.map +1 -1
- package/dist/tests/utils/test-faucet.d.ts +1 -0
- package/dist/tests/utils/test-faucet.js +9 -0
- package/dist/tests/utils/test-faucet.js.map +1 -1
- package/dist/utils/keys.d.ts +1 -1
- package/dist/utils/keys.js +4 -3
- package/dist/utils/keys.js.map +1 -1
- package/dist/utils/wasm-wrapper.js +9 -9
- package/dist/utils/wasm-wrapper.js.map +1 -1
- package/package.json +4 -3
- package/src/examples/example.js +247 -0
- package/src/examples/example.ts +207 -0
- package/src/graphql/client.ts +282 -0
- package/src/graphql/mutations/CompleteCoopExit.ts +19 -0
- package/src/graphql/mutations/CompleteLeavesSwap.ts +17 -0
- package/src/graphql/mutations/RequestCoopExit.ts +20 -0
- package/src/graphql/mutations/RequestLightningReceive.ts +26 -0
- package/src/graphql/mutations/RequestLightningSend.ts +17 -0
- package/src/graphql/mutations/RequestSwapLeaves.ts +24 -0
- package/src/graphql/objects/BitcoinNetwork.ts +22 -0
- package/src/graphql/objects/CompleteCoopExitInput.ts +41 -0
- package/src/graphql/objects/CompleteCoopExitOutput.ts +45 -0
- package/src/graphql/objects/CompleteLeavesSwapInput.ts +45 -0
- package/src/graphql/objects/CompleteLeavesSwapOutput.ts +45 -0
- package/src/graphql/objects/CompleteSeedReleaseInput.ts +41 -0
- package/src/graphql/objects/CompleteSeedReleaseOutput.ts +43 -0
- package/src/graphql/objects/Connection.ts +90 -0
- package/src/graphql/objects/CoopExitFeeEstimateInput.ts +41 -0
- package/src/graphql/objects/CoopExitFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/CoopExitRequest.ts +118 -0
- package/src/graphql/objects/CurrencyAmount.ts +74 -0
- package/src/graphql/objects/CurrencyUnit.ts +32 -0
- package/src/graphql/objects/Entity.ts +202 -0
- package/src/graphql/objects/GetChallengeInput.ts +37 -0
- package/src/graphql/objects/GetChallengeOutput.ts +43 -0
- package/src/graphql/objects/Invoice.ts +83 -0
- package/src/graphql/objects/Leaf.ts +59 -0
- package/src/graphql/objects/LeavesSwapFeeEstimateInput.ts +37 -0
- package/src/graphql/objects/LeavesSwapFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/LeavesSwapRequest.ts +192 -0
- package/src/graphql/objects/LightningReceiveFeeEstimateInput.ts +41 -0
- package/src/graphql/objects/LightningReceiveFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/LightningReceiveRequest.ts +147 -0
- package/src/graphql/objects/LightningReceiveRequestStatus.ts +34 -0
- package/src/graphql/objects/LightningSendFeeEstimateInput.ts +37 -0
- package/src/graphql/objects/LightningSendFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/LightningSendRequest.ts +134 -0
- package/src/graphql/objects/LightningSendRequestStatus.ts +28 -0
- package/src/graphql/objects/NotifyReceiverTransferInput.ts +41 -0
- package/src/graphql/objects/PageInfo.ts +58 -0
- package/src/graphql/objects/Provider.ts +41 -0
- package/src/graphql/objects/RequestCoopExitInput.ts +41 -0
- package/src/graphql/objects/RequestCoopExitOutput.ts +45 -0
- package/src/graphql/objects/RequestLeavesSwapInput.ts +55 -0
- package/src/graphql/objects/RequestLeavesSwapOutput.ts +45 -0
- package/src/graphql/objects/RequestLightningReceiveInput.ts +58 -0
- package/src/graphql/objects/RequestLightningReceiveOutput.ts +45 -0
- package/src/graphql/objects/RequestLightningSendInput.ts +41 -0
- package/src/graphql/objects/RequestLightningSendOutput.ts +45 -0
- package/src/graphql/objects/SparkCoopExitRequestStatus.ts +20 -0
- package/src/graphql/objects/SparkLeavesSwapRequestStatus.ts +20 -0
- package/src/graphql/objects/SparkTransferToLeavesConnection.ts +79 -0
- package/src/graphql/objects/SparkWalletUser.ts +86 -0
- package/src/graphql/objects/StartSeedReleaseInput.ts +37 -0
- package/src/graphql/objects/SwapLeaf.ts +53 -0
- package/src/graphql/objects/Transfer.ts +98 -0
- package/src/graphql/objects/UserLeafInput.ts +28 -0
- package/src/graphql/objects/VerifyChallengeInput.ts +51 -0
- package/src/graphql/objects/VerifyChallengeOutput.ts +43 -0
- package/src/graphql/objects/WalletUserIdentityPublicKeyInput.ts +37 -0
- package/src/graphql/objects/WalletUserIdentityPublicKeyOutput.ts +43 -0
- package/src/graphql/objects/index.ts +67 -0
- package/src/graphql/queries/CoopExitFeeEstimate.ts +18 -0
- package/src/graphql/queries/CurrentUser.ts +10 -0
- package/src/graphql/queries/LightningReceiveFeeEstimate.ts +18 -0
- package/src/graphql/queries/LightningSendFeeEstimate.ts +16 -0
- package/src/proto/common.ts +431 -0
- package/src/proto/google/protobuf/descriptor.ts +6625 -0
- package/src/proto/google/protobuf/duration.ts +197 -0
- package/src/proto/google/protobuf/empty.ts +83 -0
- package/src/proto/google/protobuf/timestamp.ts +226 -0
- package/src/proto/mock.ts +151 -0
- package/src/proto/spark.ts +12727 -0
- package/src/proto/spark_authn.ts +673 -0
- package/src/proto/validate/validate.ts +6047 -0
- package/src/services/config.ts +71 -0
- package/src/services/connection.ts +264 -0
- package/src/services/coop-exit.ts +190 -0
- package/src/services/deposit.ts +327 -0
- package/src/services/lightning.ts +341 -0
- package/src/services/lrc20.ts +42 -0
- package/src/services/token-transactions.ts +499 -0
- package/src/services/transfer.ts +1188 -0
- package/src/services/tree-creation.ts +618 -0
- package/src/services/wallet-config.ts +141 -0
- package/src/signer/signer.ts +531 -0
- package/src/spark-sdk.ts +1644 -0
- package/src/tests/adaptor-signature.test.ts +64 -0
- package/src/tests/bitcoin.test.ts +122 -0
- package/src/tests/coop-exit.test.ts +233 -0
- package/src/tests/deposit.test.ts +98 -0
- package/src/tests/keys.test.ts +82 -0
- package/src/tests/lightning.test.ts +307 -0
- package/src/tests/secret-sharing.test.ts +63 -0
- package/src/tests/swap.test.ts +252 -0
- package/src/tests/test-util.ts +92 -0
- package/src/tests/tokens.test.ts +47 -0
- package/src/tests/transfer.test.ts +371 -0
- package/src/tests/tree-creation.test.ts +56 -0
- package/src/tests/utils/spark-testing-wallet.ts +37 -0
- package/src/tests/utils/test-faucet.ts +257 -0
- package/src/types/grpc.ts +8 -0
- package/src/types/index.ts +3 -0
- package/src/utils/adaptor-signature.ts +189 -0
- package/src/utils/bitcoin.ts +138 -0
- package/src/utils/crypto.ts +14 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/keys.ts +92 -0
- package/src/utils/mempool.ts +42 -0
- package/src/utils/network.ts +70 -0
- package/src/utils/proof.ts +17 -0
- package/src/utils/response-validation.ts +26 -0
- package/src/utils/secret-sharing.ts +263 -0
- package/src/utils/signing.ts +96 -0
- package/src/utils/token-hashing.ts +163 -0
- package/src/utils/token-keyshares.ts +31 -0
- package/src/utils/token-transactions.ts +71 -0
- package/src/utils/transaction.ts +45 -0
- package/src/utils/wasm-wrapper.ts +57 -0
- package/src/utils/wasm.ts +154 -0
- package/src/wasm/spark_bindings.d.ts +208 -0
- package/src/wasm/spark_bindings.js +1161 -0
- package/src/wasm/spark_bindings_bg.wasm +0 -0
- package/src/wasm/spark_bindings_bg.wasm.d.ts +136 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
|
|
2
|
+
import * as btc from "@scure/btc-signer";
|
|
3
|
+
import { p2tr, Transaction } from "@scure/btc-signer";
|
|
4
|
+
import { equalBytes, sha256 } from "@scure/btc-signer/utils";
|
|
5
|
+
import { SignatureIntent } from "../proto/common.js";
|
|
6
|
+
import {
|
|
7
|
+
Address,
|
|
8
|
+
FinalizeNodeSignaturesResponse,
|
|
9
|
+
GenerateDepositAddressResponse,
|
|
10
|
+
StartTreeCreationResponse,
|
|
11
|
+
} from "../proto/spark.js";
|
|
12
|
+
import {
|
|
13
|
+
getP2TRAddressFromPublicKey,
|
|
14
|
+
getSigHashFromTx,
|
|
15
|
+
getTxId,
|
|
16
|
+
} from "../utils/bitcoin.js";
|
|
17
|
+
import { subtractPublicKeys } from "../utils/keys.js";
|
|
18
|
+
import { getNetwork } from "../utils/network.js";
|
|
19
|
+
import { proofOfPossessionMessageHashForDepositAddress } from "../utils/proof.js";
|
|
20
|
+
import { createWasmSigningCommitment } from "../utils/signing.js";
|
|
21
|
+
import { WalletConfigService } from "./config.js";
|
|
22
|
+
import { ConnectionManager } from "./connection.js";
|
|
23
|
+
type ValidateDepositAddressParams = {
|
|
24
|
+
address: Address;
|
|
25
|
+
userPubkey: Uint8Array;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type GenerateDepositAddressParams = {
|
|
29
|
+
signingPubkey: Uint8Array;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type CreateTreeRootParams = {
|
|
33
|
+
signingPubKey: Uint8Array;
|
|
34
|
+
verifyingKey: Uint8Array;
|
|
35
|
+
depositTx: Transaction;
|
|
36
|
+
vout: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const INITIAL_TIME_LOCK = 2000;
|
|
40
|
+
|
|
41
|
+
export class DepositService {
|
|
42
|
+
private readonly config: WalletConfigService;
|
|
43
|
+
private readonly connectionManager: ConnectionManager;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
config: WalletConfigService,
|
|
47
|
+
connectionManager: ConnectionManager,
|
|
48
|
+
) {
|
|
49
|
+
this.config = config;
|
|
50
|
+
this.connectionManager = connectionManager;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async validateDepositAddress({
|
|
54
|
+
address,
|
|
55
|
+
userPubkey,
|
|
56
|
+
}: ValidateDepositAddressParams) {
|
|
57
|
+
if (
|
|
58
|
+
!address.depositAddressProof ||
|
|
59
|
+
!address.depositAddressProof.proofOfPossessionSignature ||
|
|
60
|
+
!address.depositAddressProof.addressSignatures
|
|
61
|
+
) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"proof of possession signature or address signatures is null",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const operatorPubkey = subtractPublicKeys(address.verifyingKey, userPubkey);
|
|
68
|
+
const msg = proofOfPossessionMessageHashForDepositAddress(
|
|
69
|
+
await this.config.signer.getIdentityPublicKey(),
|
|
70
|
+
operatorPubkey,
|
|
71
|
+
address.address,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const taprootKey = p2tr(
|
|
75
|
+
operatorPubkey.slice(1, 33),
|
|
76
|
+
undefined,
|
|
77
|
+
getNetwork(this.config.getNetwork()),
|
|
78
|
+
).tweakedPubkey;
|
|
79
|
+
|
|
80
|
+
const isVerified = schnorr.verify(
|
|
81
|
+
address.depositAddressProof.proofOfPossessionSignature,
|
|
82
|
+
msg,
|
|
83
|
+
taprootKey,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!isVerified) {
|
|
87
|
+
throw new Error("proof of possession signature verification failed");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const addrHash = sha256(address.address);
|
|
91
|
+
for (const operator of Object.values(this.config.getSigningOperators())) {
|
|
92
|
+
if (operator.identifier === this.config.getCoordinatorIdentifier()) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const operatorPubkey = operator.identityPublicKey;
|
|
97
|
+
const operatorSig =
|
|
98
|
+
address.depositAddressProof.addressSignatures[operator.identifier];
|
|
99
|
+
if (!operatorSig) {
|
|
100
|
+
throw new Error("operator signature not found");
|
|
101
|
+
}
|
|
102
|
+
const sig = secp256k1.Signature.fromDER(operatorSig);
|
|
103
|
+
|
|
104
|
+
const isVerified = secp256k1.verify(sig, addrHash, operatorPubkey);
|
|
105
|
+
if (!isVerified) {
|
|
106
|
+
throw new Error("signature verification failed");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async generateDepositAddress({
|
|
112
|
+
signingPubkey,
|
|
113
|
+
}: GenerateDepositAddressParams): Promise<GenerateDepositAddressResponse> {
|
|
114
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
115
|
+
this.config.getCoordinatorAddress(),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
let depositResp: GenerateDepositAddressResponse;
|
|
119
|
+
try {
|
|
120
|
+
depositResp = await sparkClient.generate_deposit_address({
|
|
121
|
+
signingPublicKey: signingPubkey,
|
|
122
|
+
identityPublicKey: await this.config.signer.getIdentityPublicKey(),
|
|
123
|
+
network: this.config.getNetworkProto(),
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error(`Error generating deposit address: ${error}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!depositResp.depositAddress) {
|
|
130
|
+
throw new Error("No deposit address response from coordinator");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await this.validateDepositAddress({
|
|
134
|
+
address: depositResp.depositAddress,
|
|
135
|
+
userPubkey: signingPubkey,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return depositResp;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async createTreeRoot({
|
|
142
|
+
signingPubKey,
|
|
143
|
+
verifyingKey,
|
|
144
|
+
depositTx,
|
|
145
|
+
vout,
|
|
146
|
+
}: CreateTreeRootParams) {
|
|
147
|
+
// Create a root tx
|
|
148
|
+
const rootTx = new Transaction();
|
|
149
|
+
const output = depositTx.getOutput(vout);
|
|
150
|
+
if (!output) {
|
|
151
|
+
throw new Error("No output found in deposit tx");
|
|
152
|
+
}
|
|
153
|
+
const script = output.script;
|
|
154
|
+
const amount = output.amount;
|
|
155
|
+
if (!script || !amount) {
|
|
156
|
+
throw new Error("No script or amount found in deposit tx");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
rootTx.addInput({
|
|
160
|
+
txid: getTxId(depositTx),
|
|
161
|
+
index: vout,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
rootTx.addOutput({
|
|
165
|
+
script,
|
|
166
|
+
amount,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const rootNonceCommitment =
|
|
170
|
+
await this.config.signer.getRandomSigningCommitment();
|
|
171
|
+
const rootTxSighash = getSigHashFromTx(rootTx, 0, output);
|
|
172
|
+
|
|
173
|
+
// Create a refund tx
|
|
174
|
+
const refundTx = new Transaction();
|
|
175
|
+
const sequence = (1 << 30) | INITIAL_TIME_LOCK;
|
|
176
|
+
refundTx.addInput({
|
|
177
|
+
txid: getTxId(rootTx),
|
|
178
|
+
index: 0,
|
|
179
|
+
sequence,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const refundP2trAddress = getP2TRAddressFromPublicKey(
|
|
183
|
+
signingPubKey,
|
|
184
|
+
this.config.getNetwork(),
|
|
185
|
+
);
|
|
186
|
+
const refundAddress = btc
|
|
187
|
+
.Address(getNetwork(this.config.getNetwork()))
|
|
188
|
+
.decode(refundP2trAddress);
|
|
189
|
+
const refundPkScript = btc.OutScript.encode(refundAddress);
|
|
190
|
+
|
|
191
|
+
refundTx.addOutput({
|
|
192
|
+
script: refundPkScript,
|
|
193
|
+
amount: amount,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const refundNonceCommitment =
|
|
197
|
+
await this.config.signer.getRandomSigningCommitment();
|
|
198
|
+
const refundTxSighash = getSigHashFromTx(refundTx, 0, output);
|
|
199
|
+
|
|
200
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
201
|
+
this.config.getCoordinatorAddress(),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
let treeResp: StartTreeCreationResponse;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
treeResp = await sparkClient.start_tree_creation({
|
|
208
|
+
identityPublicKey: await this.config.signer.getIdentityPublicKey(),
|
|
209
|
+
onChainUtxo: {
|
|
210
|
+
vout: vout,
|
|
211
|
+
rawTx: depositTx.toBytes(),
|
|
212
|
+
network: this.config.getNetworkProto(),
|
|
213
|
+
},
|
|
214
|
+
rootTxSigningJob: {
|
|
215
|
+
rawTx: rootTx.toBytes(),
|
|
216
|
+
signingPublicKey: signingPubKey,
|
|
217
|
+
signingNonceCommitment: rootNonceCommitment,
|
|
218
|
+
},
|
|
219
|
+
refundTxSigningJob: {
|
|
220
|
+
rawTx: refundTx.toBytes(),
|
|
221
|
+
signingPublicKey: signingPubKey,
|
|
222
|
+
signingNonceCommitment: refundNonceCommitment,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw new Error(`Error starting tree creation: ${error}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!treeResp.rootNodeSignatureShares?.verifyingKey) {
|
|
230
|
+
throw new Error("No verifying key found in tree response");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
!treeResp.rootNodeSignatureShares.nodeTxSigningResult
|
|
235
|
+
?.signingNonceCommitments
|
|
236
|
+
) {
|
|
237
|
+
throw new Error("No signing nonce commitments found in tree response");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (
|
|
241
|
+
!treeResp.rootNodeSignatureShares.refundTxSigningResult
|
|
242
|
+
?.signingNonceCommitments
|
|
243
|
+
) {
|
|
244
|
+
throw new Error("No signing nonce commitments found in tree response");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
!equalBytes(treeResp.rootNodeSignatureShares.verifyingKey, verifyingKey)
|
|
249
|
+
) {
|
|
250
|
+
throw new Error("Verifying key does not match");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const rootSignature = await this.config.signer.signFrost({
|
|
254
|
+
message: rootTxSighash,
|
|
255
|
+
publicKey: signingPubKey,
|
|
256
|
+
privateAsPubKey: signingPubKey,
|
|
257
|
+
verifyingKey,
|
|
258
|
+
selfCommitment: rootNonceCommitment,
|
|
259
|
+
statechainCommitments:
|
|
260
|
+
treeResp.rootNodeSignatureShares.nodeTxSigningResult
|
|
261
|
+
.signingNonceCommitments,
|
|
262
|
+
adaptorPubKey: new Uint8Array(),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const refundSignature = await this.config.signer.signFrost({
|
|
266
|
+
message: refundTxSighash,
|
|
267
|
+
publicKey: signingPubKey,
|
|
268
|
+
privateAsPubKey: signingPubKey,
|
|
269
|
+
verifyingKey,
|
|
270
|
+
selfCommitment: refundNonceCommitment,
|
|
271
|
+
statechainCommitments:
|
|
272
|
+
treeResp.rootNodeSignatureShares.refundTxSigningResult
|
|
273
|
+
.signingNonceCommitments,
|
|
274
|
+
adaptorPubKey: new Uint8Array(),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const rootAggregate = await this.config.signer.aggregateFrost({
|
|
278
|
+
message: rootTxSighash,
|
|
279
|
+
statechainSignatures:
|
|
280
|
+
treeResp.rootNodeSignatureShares.nodeTxSigningResult.signatureShares,
|
|
281
|
+
statechainPublicKeys:
|
|
282
|
+
treeResp.rootNodeSignatureShares.nodeTxSigningResult.publicKeys,
|
|
283
|
+
verifyingKey: treeResp.rootNodeSignatureShares.verifyingKey,
|
|
284
|
+
statechainCommitments:
|
|
285
|
+
treeResp.rootNodeSignatureShares.nodeTxSigningResult
|
|
286
|
+
.signingNonceCommitments,
|
|
287
|
+
selfCommitment: createWasmSigningCommitment(rootNonceCommitment),
|
|
288
|
+
publicKey: signingPubKey,
|
|
289
|
+
selfSignature: rootSignature!,
|
|
290
|
+
adaptorPubKey: new Uint8Array(),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const refundAggregate = await this.config.signer.aggregateFrost({
|
|
294
|
+
message: refundTxSighash,
|
|
295
|
+
statechainSignatures:
|
|
296
|
+
treeResp.rootNodeSignatureShares.refundTxSigningResult.signatureShares,
|
|
297
|
+
statechainPublicKeys:
|
|
298
|
+
treeResp.rootNodeSignatureShares.refundTxSigningResult.publicKeys,
|
|
299
|
+
verifyingKey: treeResp.rootNodeSignatureShares.verifyingKey,
|
|
300
|
+
statechainCommitments:
|
|
301
|
+
treeResp.rootNodeSignatureShares.refundTxSigningResult
|
|
302
|
+
.signingNonceCommitments,
|
|
303
|
+
selfCommitment: createWasmSigningCommitment(refundNonceCommitment),
|
|
304
|
+
publicKey: signingPubKey,
|
|
305
|
+
selfSignature: refundSignature,
|
|
306
|
+
adaptorPubKey: new Uint8Array(),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
let finalizeResp: FinalizeNodeSignaturesResponse;
|
|
310
|
+
try {
|
|
311
|
+
finalizeResp = await sparkClient.finalize_node_signatures({
|
|
312
|
+
intent: SignatureIntent.CREATION,
|
|
313
|
+
nodeSignatures: [
|
|
314
|
+
{
|
|
315
|
+
nodeId: treeResp.rootNodeSignatureShares.nodeId,
|
|
316
|
+
nodeTxSignature: rootAggregate,
|
|
317
|
+
refundTxSignature: refundAggregate,
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
});
|
|
321
|
+
} catch (error) {
|
|
322
|
+
throw new Error(`Error finalizing node signatures in deposit: ${error}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return finalizeResp;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bytesToNumberBE,
|
|
3
|
+
hexToBytes,
|
|
4
|
+
numberToBytesBE,
|
|
5
|
+
} from "@noble/curves/abstract/utils";
|
|
6
|
+
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
7
|
+
import { TransactionInput } from "@scure/btc-signer/psbt";
|
|
8
|
+
import { sha256 } from "@scure/btc-signer/utils";
|
|
9
|
+
import { decode } from "light-bolt11-decoder";
|
|
10
|
+
import {
|
|
11
|
+
GetSigningCommitmentsResponse,
|
|
12
|
+
InitiatePreimageSwapRequest_Reason,
|
|
13
|
+
InitiatePreimageSwapResponse,
|
|
14
|
+
ProvidePreimageResponse,
|
|
15
|
+
QueryUserSignedRefundsResponse,
|
|
16
|
+
RequestedSigningCommitments,
|
|
17
|
+
Transfer,
|
|
18
|
+
UserSignedRefund,
|
|
19
|
+
} from "../proto/spark.js";
|
|
20
|
+
import {
|
|
21
|
+
getSigHashFromTx,
|
|
22
|
+
getTxFromRawTxBytes,
|
|
23
|
+
getTxId,
|
|
24
|
+
} from "../utils/bitcoin.js";
|
|
25
|
+
import { getCrypto } from "../utils/crypto.js";
|
|
26
|
+
import {
|
|
27
|
+
createRefundTx,
|
|
28
|
+
getNextTransactionSequence,
|
|
29
|
+
} from "../utils/transaction.js";
|
|
30
|
+
import { WalletConfigService } from "./config.js";
|
|
31
|
+
import { ConnectionManager } from "./connection.js";
|
|
32
|
+
import { LeafKeyTweak } from "./transfer.js";
|
|
33
|
+
|
|
34
|
+
const crypto = getCrypto();
|
|
35
|
+
|
|
36
|
+
export type CreateLightningInvoiceParams = {
|
|
37
|
+
invoiceCreator: (
|
|
38
|
+
amountSats: number,
|
|
39
|
+
paymentHash: Uint8Array,
|
|
40
|
+
memo: string,
|
|
41
|
+
) => Promise<string | undefined>;
|
|
42
|
+
amountSats: number;
|
|
43
|
+
memo: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type CreateLightningInvoiceWithPreimageParams = {
|
|
47
|
+
preimage: Uint8Array;
|
|
48
|
+
} & CreateLightningInvoiceParams;
|
|
49
|
+
|
|
50
|
+
export type SwapNodesForPreimageParams = {
|
|
51
|
+
leaves: LeafKeyTweak[];
|
|
52
|
+
receiverIdentityPubkey: Uint8Array;
|
|
53
|
+
paymentHash: Uint8Array;
|
|
54
|
+
invoiceString?: string;
|
|
55
|
+
isInboundPayment: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export class LightningService {
|
|
59
|
+
private readonly config: WalletConfigService;
|
|
60
|
+
private readonly connectionManager: ConnectionManager;
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
config: WalletConfigService,
|
|
64
|
+
connectionManager: ConnectionManager,
|
|
65
|
+
) {
|
|
66
|
+
this.config = config;
|
|
67
|
+
this.connectionManager = connectionManager;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createLightningInvoice({
|
|
71
|
+
invoiceCreator,
|
|
72
|
+
amountSats,
|
|
73
|
+
memo,
|
|
74
|
+
}: CreateLightningInvoiceParams): Promise<string> {
|
|
75
|
+
const randBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
76
|
+
const preimage = numberToBytesBE(
|
|
77
|
+
bytesToNumberBE(randBytes) % secp256k1.CURVE.n,
|
|
78
|
+
32,
|
|
79
|
+
);
|
|
80
|
+
return await this.createLightningInvoiceWithPreImage({
|
|
81
|
+
invoiceCreator,
|
|
82
|
+
amountSats,
|
|
83
|
+
memo,
|
|
84
|
+
preimage,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async createLightningInvoiceWithPreImage({
|
|
89
|
+
invoiceCreator,
|
|
90
|
+
amountSats,
|
|
91
|
+
memo,
|
|
92
|
+
preimage,
|
|
93
|
+
}: CreateLightningInvoiceWithPreimageParams): Promise<string> {
|
|
94
|
+
const paymentHash = sha256(preimage);
|
|
95
|
+
const invoice = await invoiceCreator(amountSats, paymentHash, memo);
|
|
96
|
+
if (!invoice) {
|
|
97
|
+
throw new Error("Error creating lightning invoice");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const shares = await this.config.signer.splitSecretWithProofs({
|
|
101
|
+
secret: preimage,
|
|
102
|
+
curveOrder: secp256k1.CURVE.n,
|
|
103
|
+
threshold: this.config.getThreshold(),
|
|
104
|
+
numShares: Object.keys(this.config.getSigningOperators()).length,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const errors: Error[] = [];
|
|
108
|
+
const promises = Object.entries(this.config.getSigningOperators()).map(
|
|
109
|
+
async ([_, operator]) => {
|
|
110
|
+
const share = shares[operator.id];
|
|
111
|
+
if (!share) {
|
|
112
|
+
throw new Error("Share not found");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
116
|
+
operator.address,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await sparkClient.store_preimage_share({
|
|
121
|
+
paymentHash,
|
|
122
|
+
preimageShare: {
|
|
123
|
+
secretShare: numberToBytesBE(share.share, 32),
|
|
124
|
+
proofs: share.proofs,
|
|
125
|
+
},
|
|
126
|
+
threshold: this.config.getThreshold(),
|
|
127
|
+
invoiceString: invoice,
|
|
128
|
+
userIdentityPublicKey:
|
|
129
|
+
await this.config.signer.getIdentityPublicKey(),
|
|
130
|
+
});
|
|
131
|
+
} catch (e: any) {
|
|
132
|
+
errors.push(e);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
await Promise.all(promises);
|
|
138
|
+
|
|
139
|
+
if (errors.length > 0) {
|
|
140
|
+
throw new Error(`Error creating lightning invoice: ${errors[0]}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return invoice;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async swapNodesForPreimage({
|
|
147
|
+
leaves,
|
|
148
|
+
receiverIdentityPubkey,
|
|
149
|
+
paymentHash,
|
|
150
|
+
invoiceString,
|
|
151
|
+
isInboundPayment,
|
|
152
|
+
}: SwapNodesForPreimageParams): Promise<InitiatePreimageSwapResponse> {
|
|
153
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
154
|
+
this.config.getCoordinatorAddress(),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
let signingCommitments: GetSigningCommitmentsResponse;
|
|
158
|
+
try {
|
|
159
|
+
signingCommitments = await sparkClient.get_signing_commitments({
|
|
160
|
+
nodeIds: leaves.map((leaf) => leaf.leaf.id),
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw new Error(`Error getting signing commitments: ${error}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const userSignedRefunds = await this.signRefunds(
|
|
167
|
+
leaves,
|
|
168
|
+
signingCommitments.signingCommitments,
|
|
169
|
+
receiverIdentityPubkey,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const transferId = crypto.randomUUID();
|
|
173
|
+
let bolt11String = "";
|
|
174
|
+
let amountSats: number = 0;
|
|
175
|
+
if (invoiceString) {
|
|
176
|
+
const decodedInvoice = decode(invoiceString);
|
|
177
|
+
let amountMsats = 0;
|
|
178
|
+
try {
|
|
179
|
+
amountMsats = Number(
|
|
180
|
+
decodedInvoice.sections.find((section) => section.name === "amount")
|
|
181
|
+
?.value,
|
|
182
|
+
);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("Error decoding invoice", error);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
amountSats = amountMsats / 1000;
|
|
188
|
+
bolt11String = invoiceString;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const reason = isInboundPayment
|
|
192
|
+
? InitiatePreimageSwapRequest_Reason.REASON_RECEIVE
|
|
193
|
+
: InitiatePreimageSwapRequest_Reason.REASON_SEND;
|
|
194
|
+
|
|
195
|
+
let response: InitiatePreimageSwapResponse;
|
|
196
|
+
try {
|
|
197
|
+
response = await sparkClient.initiate_preimage_swap({
|
|
198
|
+
paymentHash,
|
|
199
|
+
userSignedRefunds,
|
|
200
|
+
reason,
|
|
201
|
+
invoiceAmount: {
|
|
202
|
+
invoiceAmountProof: {
|
|
203
|
+
bolt11Invoice: bolt11String,
|
|
204
|
+
},
|
|
205
|
+
valueSats: amountSats,
|
|
206
|
+
},
|
|
207
|
+
transfer: {
|
|
208
|
+
transferId,
|
|
209
|
+
ownerIdentityPublicKey:
|
|
210
|
+
await this.config.signer.getIdentityPublicKey(),
|
|
211
|
+
receiverIdentityPublicKey: receiverIdentityPubkey,
|
|
212
|
+
expiryTime: new Date(Date.now() + 2 * 60 * 1000),
|
|
213
|
+
},
|
|
214
|
+
receiverIdentityPublicKey: receiverIdentityPubkey,
|
|
215
|
+
feeSats: 0,
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw new Error(`Error initiating preimage swap: ${error}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return response;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async queryUserSignedRefunds(
|
|
225
|
+
paymentHash: Uint8Array,
|
|
226
|
+
): Promise<UserSignedRefund[]> {
|
|
227
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
228
|
+
this.config.getCoordinatorAddress(),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
let response: QueryUserSignedRefundsResponse;
|
|
232
|
+
try {
|
|
233
|
+
response = await sparkClient.query_user_signed_refunds({
|
|
234
|
+
paymentHash,
|
|
235
|
+
});
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw new Error(`Error querying user signed refunds: ${error}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return response.userSignedRefunds;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
validateUserSignedRefund(userSignedRefund: UserSignedRefund): bigint {
|
|
244
|
+
const refundTx = getTxFromRawTxBytes(userSignedRefund.refundTx);
|
|
245
|
+
// TODO: Should we assert that the amount is always defined here?
|
|
246
|
+
return refundTx.getOutput(0).amount || 0n;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async providePreimage(preimage: Uint8Array): Promise<Transfer> {
|
|
250
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
251
|
+
this.config.getCoordinatorAddress(),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const paymentHash = sha256(preimage);
|
|
255
|
+
let response: ProvidePreimageResponse;
|
|
256
|
+
try {
|
|
257
|
+
response = await sparkClient.provide_preimage({
|
|
258
|
+
preimage,
|
|
259
|
+
paymentHash,
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw new Error(`Error providing preimage: ${error}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!response.transfer) {
|
|
266
|
+
throw new Error("No transfer returned from coordinator");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return response.transfer;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async signRefunds(
|
|
273
|
+
leaves: LeafKeyTweak[],
|
|
274
|
+
signingCommitments: RequestedSigningCommitments[],
|
|
275
|
+
receiverIdentityPubkey: Uint8Array,
|
|
276
|
+
): Promise<UserSignedRefund[]> {
|
|
277
|
+
const userSignedRefunds: UserSignedRefund[] = [];
|
|
278
|
+
for (let i = 0; i < leaves.length; i++) {
|
|
279
|
+
const leaf = leaves[i];
|
|
280
|
+
if (!leaf?.leaf) {
|
|
281
|
+
throw new Error("Leaf not found in signRefunds");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const nodeTx = getTxFromRawTxBytes(leaf.leaf.nodeTx);
|
|
285
|
+
const nodeOutPoint: TransactionInput = {
|
|
286
|
+
txid: hexToBytes(getTxId(nodeTx)),
|
|
287
|
+
index: 0,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const currRefundTx = getTxFromRawTxBytes(leaf.leaf.refundTx);
|
|
291
|
+
const nextSequence = getNextTransactionSequence(
|
|
292
|
+
currRefundTx.getInput(0).sequence,
|
|
293
|
+
);
|
|
294
|
+
const amountSats = currRefundTx.getOutput(0).amount;
|
|
295
|
+
if (amountSats === undefined) {
|
|
296
|
+
throw new Error("Amount not found in signRefunds");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const refundTx = createRefundTx(
|
|
300
|
+
nextSequence,
|
|
301
|
+
nodeOutPoint,
|
|
302
|
+
amountSats,
|
|
303
|
+
receiverIdentityPubkey,
|
|
304
|
+
this.config.getNetwork(),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const sighash = getSigHashFromTx(refundTx, 0, nodeTx.getOutput(0));
|
|
308
|
+
|
|
309
|
+
const signingCommitment =
|
|
310
|
+
await this.config.signer.getRandomSigningCommitment();
|
|
311
|
+
|
|
312
|
+
const signingNonceCommitments =
|
|
313
|
+
signingCommitments[i]?.signingNonceCommitments;
|
|
314
|
+
if (!signingNonceCommitments) {
|
|
315
|
+
throw new Error("Signing nonce commitments not found in signRefunds");
|
|
316
|
+
}
|
|
317
|
+
const signingResult = await this.config.signer.signFrost({
|
|
318
|
+
message: sighash,
|
|
319
|
+
publicKey: leaf.signingPubKey,
|
|
320
|
+
privateAsPubKey: leaf.signingPubKey,
|
|
321
|
+
selfCommitment: signingCommitment,
|
|
322
|
+
statechainCommitments: signingNonceCommitments,
|
|
323
|
+
adaptorPubKey: new Uint8Array(),
|
|
324
|
+
verifyingKey: leaf.leaf.verifyingPublicKey,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
userSignedRefunds.push({
|
|
328
|
+
nodeId: leaf.leaf.id,
|
|
329
|
+
refundTx: refundTx.toBytes(),
|
|
330
|
+
userSignature: signingResult,
|
|
331
|
+
userSignatureCommitment: signingCommitment,
|
|
332
|
+
signingCommitments: {
|
|
333
|
+
signingCommitments: signingNonceCommitments,
|
|
334
|
+
},
|
|
335
|
+
network: this.config.getNetworkProto(),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return userSignedRefunds;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { LRCWallet, Lrc20TransactionDto } from "@buildonspark/lrc20-sdk";
|
|
2
|
+
|
|
3
|
+
import { bytesToHex, bytesToNumberBE } from "@noble/curves/abstract/utils";
|
|
4
|
+
import { LeafWithPreviousTransactionData } from "../proto/spark.js";
|
|
5
|
+
|
|
6
|
+
//TODO: dynamically set these for each leaf based on its metadata in the transaction that created it
|
|
7
|
+
const WITHDRAW_BOND_SATS = 10000;
|
|
8
|
+
const WITHDRAW_RELATIVE_BLOCK_LOCKTIME = 100;
|
|
9
|
+
|
|
10
|
+
export async function broadcastL1Withdrawal(
|
|
11
|
+
lrcWallet: LRCWallet,
|
|
12
|
+
leavesToExit: LeafWithPreviousTransactionData[],
|
|
13
|
+
receiverPublicKey: string,
|
|
14
|
+
feeRateSatsPerVb: number = 2.0,
|
|
15
|
+
): Promise<{ txid: string }> {
|
|
16
|
+
await lrcWallet.syncWallet();
|
|
17
|
+
|
|
18
|
+
let payments = leavesToExit.map(
|
|
19
|
+
({ leaf, previousTransactionHash, previousTransactionVout }) => {
|
|
20
|
+
return {
|
|
21
|
+
amount: bytesToNumberBE(leaf!.tokenAmount),
|
|
22
|
+
tokenPubkey: bytesToHex(leaf!.tokenPublicKey),
|
|
23
|
+
sats: WITHDRAW_BOND_SATS,
|
|
24
|
+
cltvOutputLocktime: WITHDRAW_RELATIVE_BLOCK_LOCKTIME,
|
|
25
|
+
revocationKey: bytesToHex(leaf!.revocationPublicKey!),
|
|
26
|
+
expiryKey: receiverPublicKey,
|
|
27
|
+
metadata: {
|
|
28
|
+
token_tx_hash: bytesToHex(previousTransactionHash),
|
|
29
|
+
exit_leaf_index: previousTransactionVout,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const tx = await lrcWallet.prepareSparkExit(payments, feeRateSatsPerVb);
|
|
36
|
+
|
|
37
|
+
let txDto = Lrc20TransactionDto.fromLrc20Transaction(tx);
|
|
38
|
+
|
|
39
|
+
let txid = await lrcWallet.broadcast(txDto); //.broadcastRawBtcTransaction(tx.bitcoin_tx.toHex());
|
|
40
|
+
|
|
41
|
+
return { txid };
|
|
42
|
+
}
|