@utxopia/sdk 0.1.0-alpha.2 → 0.1.0-alpha.3
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/README.md +214 -108
- package/{packages/sdk/dist → dist}/client.d.ts +25 -1
- package/{packages/sdk/dist → dist}/client.js +36 -1
- package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
- package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
- package/{packages/sdk/dist → dist}/index.d.ts +3 -3
- package/{packages/sdk/dist → dist}/index.js +3 -3
- package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
- package/{packages/sdk/dist → dist}/instructions.js +45 -15
- package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
- package/{packages/sdk/dist → dist}/stealth.js +146 -14
- package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
- package/{packages/sdk/dist → dist}/taproot.js +54 -2
- package/package.json +86 -63
- package/src/announcement-client.ts +457 -0
- package/src/auditor-ciphertext.ts +181 -0
- package/src/auditor.ts +409 -0
- package/src/bitcoin/ika.ts +103 -0
- package/src/bitcoin/index.ts +5 -0
- package/src/bound-params.ts +322 -0
- package/src/chadbuffer.ts +603 -0
- package/src/circomlibjs.d.ts +51 -0
- package/src/claim-link.ts +53 -0
- package/src/client.ts +638 -0
- package/src/commitment-tree.ts +736 -0
- package/src/config.ts +772 -0
- package/src/core/esplora.ts +332 -0
- package/src/core/mempool.ts +159 -0
- package/src/crypto-babyjub.ts +385 -0
- package/src/crypto-ed25519.ts +297 -0
- package/src/crypto.ts +199 -0
- package/src/event-client.ts +231 -0
- package/src/events.ts +384 -0
- package/src/explorer.ts +300 -0
- package/src/index.ts +902 -0
- package/src/instructions.ts +2820 -0
- package/src/keys.ts +1228 -0
- package/src/logger.ts +41 -0
- package/src/magicblock.ts +278 -0
- package/src/merkle.ts +197 -0
- package/src/note.ts +754 -0
- package/src/pda.ts +516 -0
- package/src/pool-state.ts +176 -0
- package/src/poseidon.ts +175 -0
- package/src/prover/index.ts +19 -0
- package/src/prover/mobile.ts +303 -0
- package/src/prover/web.ts +771 -0
- package/src/psbt.ts +333 -0
- package/src/selective-disclosure.ts +284 -0
- package/src/sender-memo.ts +343 -0
- package/src/snarkjs.d.ts +19 -0
- package/src/sns-resolver.ts +333 -0
- package/src/solana/connection.ts +189 -0
- package/src/solana/priority-fee.ts +201 -0
- package/src/spend-doc.ts +163 -0
- package/src/stealth.ts +1477 -0
- package/src/taproot.ts +707 -0
- package/src/token-registry.ts +207 -0
- package/src/utils/encoding.ts +33 -0
- package/src/vk-registry.ts +295 -0
- package/LICENSE +0 -21
- package/packages/btc-client/src/esplora-client.ts +0 -153
- package/packages/btc-client/src/index.ts +0 -3
- package/packages/btc-client/src/op-return.ts +0 -93
- package/packages/btc-client/src/types.ts +0 -112
- package/packages/sdk/README.md +0 -277
- /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
- /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/config.js +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto.js +0 -0
- /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/event-client.js +0 -0
- /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/events.js +0 -0
- /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/explorer.js +0 -0
- /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/keys.js +0 -0
- /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/logger.js +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
- /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/merkle.js +0 -0
- /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/note.js +0 -0
- /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pda.js +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
- /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/psbt.js +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
|
@@ -33,7 +33,7 @@ export declare const INSTRUCTION_DISCRIMINATORS: {
|
|
|
33
33
|
readonly COMPLETE_DEPOSIT: 11;
|
|
34
34
|
readonly SHIELD: 12;
|
|
35
35
|
/** OP_RETURN-free deposit: note keys ride in instruction data, proven by the
|
|
36
|
-
* deposit address's
|
|
36
|
+
* deposit address's tapleaf. */
|
|
37
37
|
readonly VERIFY_DEPOSIT: 25;
|
|
38
38
|
readonly TRANSACT: 13;
|
|
39
39
|
readonly UNSHIELD: 14;
|
|
@@ -762,16 +762,19 @@ export declare function buildCompleteDepositInstructionData(params: {
|
|
|
762
762
|
*
|
|
763
763
|
* The OP_RETURN-free deposit path. `notePublicKey` + `ephemeralPubkey` travel in
|
|
764
764
|
* instruction data instead of in the Bitcoin transaction, and the program proves
|
|
765
|
-
* them against the deposit output's
|
|
766
|
-
* a different address, which the funding transaction did
|
|
767
|
-
* the deposit as a UTXOpia transaction on chain, so any
|
|
768
|
-
* can send to a P2TR address can fund it.
|
|
765
|
+
* them against the deposit output's tapleaf — a different key pair derives a
|
|
766
|
+
* different leaf, and so a different address, which the funding transaction did
|
|
767
|
+
* not pay. Nothing marks the deposit as a UTXOpia transaction on chain, so any
|
|
768
|
+
* wallet or exchange that can send to a P2TR address can fund it.
|
|
769
769
|
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
* an ephemeral key that leaves the note undiscoverable.
|
|
770
|
+
* Derive the address with `deriveDepositAddress(depositTweakCommitment(npk, eph),
|
|
771
|
+
* ikaXOnlyPubkey)`. Both keys are hashed into the leaf, so a caller cannot swap
|
|
772
|
+
* in an ephemeral key that leaves the note undiscoverable.
|
|
773
773
|
*
|
|
774
|
-
*
|
|
774
|
+
* No sweep: the deposit output's tapleaf names the pool's own dWallet key, so it
|
|
775
|
+
* is already under pool custody and is recorded as a pool UTXO directly. The
|
|
776
|
+
* SPV-verified transaction must therefore BE the deposit — `depositTxSize` is 0
|
|
777
|
+
* and `depositTxid` defaults to `sweepTxid`. The receipt PDA is seeded
|
|
775
778
|
* `["deposit_receipt", txid, vout]`, so pass `depositVout` to
|
|
776
779
|
* `deriveDepositReceiptPDA` for this flow.
|
|
777
780
|
*
|
|
@@ -783,12 +786,26 @@ export declare function buildVerifyDepositInstructionData(params: {
|
|
|
783
786
|
sweepTxid: Uint8Array;
|
|
784
787
|
blockHeight: number;
|
|
785
788
|
sweepTxSize: number;
|
|
786
|
-
depositTxSize
|
|
787
|
-
depositTxid
|
|
789
|
+
depositTxSize?: number;
|
|
790
|
+
depositTxid?: Uint8Array;
|
|
788
791
|
ephemeralPubkey: Uint8Array;
|
|
789
792
|
notePublicKey: Uint8Array;
|
|
790
793
|
depositVout: number;
|
|
791
794
|
}): Uint8Array;
|
|
795
|
+
/**
|
|
796
|
+
* Build utxopia verify_deposit_permissioned instruction data (disc=26).
|
|
797
|
+
*
|
|
798
|
+
* `buildVerifyDepositInstructionData`'s payload with a different discriminator
|
|
799
|
+
* and an auditor ciphertext appended. The tapleaf already tells the pools apart —
|
|
800
|
+
* each carries its own Ika custody key — but that is not the same as clearing the
|
|
801
|
+
* pool's policy, which is what a permissioned pool exists for.
|
|
802
|
+
*
|
|
803
|
+
* The one-time PolicyApproval is bound to the WHOLE payload, ciphertext included,
|
|
804
|
+
* so these exact bytes must be the ones approved.
|
|
805
|
+
*/
|
|
806
|
+
export declare function buildVerifyDepositPermissionedInstructionData(params: Parameters<typeof buildVerifyDepositInstructionData>[0] & {
|
|
807
|
+
auditorCiphertext?: Uint8Array;
|
|
808
|
+
}): Uint8Array;
|
|
792
809
|
/** PoolConfig account discriminator (0x0a) */
|
|
793
810
|
export declare const POOL_CONFIG_DISCRIMINATOR = 10;
|
|
794
811
|
/** Serialized PoolConfig account length (bytes) */
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { AccountRole, } from "@solana/kit";
|
|
10
10
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
11
|
+
import { bytesToHex as toHex } from "./crypto";
|
|
11
12
|
import { address, getConfig, TOKEN_2022_PROGRAM_ID } from "./config";
|
|
12
13
|
import { resolveAuditorCiphertext } from "./auditor-ciphertext";
|
|
13
14
|
import { MAGICBLOCK_EPHEMERAL_VAULT_ID, MAGICBLOCK_DELEGATION_PROGRAM_ID, MAGICBLOCK_MAGIC_CONTEXT_ID, MAGICBLOCK_MAGIC_PROGRAM_ID, MAGICBLOCK_MAX_PER_MEMBERS, MAGICBLOCK_PERMISSION_PROGRAM_ID, MAGICBLOCK_PER_MEMBER_FLAGS, } from "./magicblock";
|
|
@@ -37,7 +38,7 @@ const INSTRUCTION = {
|
|
|
37
38
|
COMPLETE_DEPOSIT: 11,
|
|
38
39
|
SHIELD: 12,
|
|
39
40
|
/** OP_RETURN-free deposit: note keys ride in instruction data, proven by the
|
|
40
|
-
* deposit address's
|
|
41
|
+
* deposit address's tapleaf. */
|
|
41
42
|
VERIFY_DEPOSIT: 25,
|
|
42
43
|
// JoinSplit (13-15) — all share n_in + n_out + n_pub + proof_source header
|
|
43
44
|
TRANSACT: 13,
|
|
@@ -72,6 +73,8 @@ const INSTRUCTION = {
|
|
|
72
73
|
const PERMISSIONED_DISC = {
|
|
73
74
|
INITIALIZE_PERMISSIONED: 21,
|
|
74
75
|
COMPLETE_DEPOSIT_PERMISSIONED: 22,
|
|
76
|
+
/** Same binding as VERIFY_DEPOSIT, plus the permissioned pool's policy gate. */
|
|
77
|
+
VERIFY_DEPOSIT_PERMISSIONED: 26,
|
|
75
78
|
SHIELD_PERMISSIONED: 23,
|
|
76
79
|
REGISTER_EXIT_DESTINATION: 39,
|
|
77
80
|
};
|
|
@@ -1311,16 +1314,19 @@ export function buildCompleteDepositInstructionData(params) {
|
|
|
1311
1314
|
*
|
|
1312
1315
|
* The OP_RETURN-free deposit path. `notePublicKey` + `ephemeralPubkey` travel in
|
|
1313
1316
|
* instruction data instead of in the Bitcoin transaction, and the program proves
|
|
1314
|
-
* them against the deposit output's
|
|
1315
|
-
* a different address, which the funding transaction did
|
|
1316
|
-
* the deposit as a UTXOpia transaction on chain, so any
|
|
1317
|
-
* can send to a P2TR address can fund it.
|
|
1317
|
+
* them against the deposit output's tapleaf — a different key pair derives a
|
|
1318
|
+
* different leaf, and so a different address, which the funding transaction did
|
|
1319
|
+
* not pay. Nothing marks the deposit as a UTXOpia transaction on chain, so any
|
|
1320
|
+
* wallet or exchange that can send to a P2TR address can fund it.
|
|
1318
1321
|
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1321
|
-
* an ephemeral key that leaves the note undiscoverable.
|
|
1322
|
+
* Derive the address with `deriveDepositAddress(depositTweakCommitment(npk, eph),
|
|
1323
|
+
* ikaXOnlyPubkey)`. Both keys are hashed into the leaf, so a caller cannot swap
|
|
1324
|
+
* in an ephemeral key that leaves the note undiscoverable.
|
|
1322
1325
|
*
|
|
1323
|
-
*
|
|
1326
|
+
* No sweep: the deposit output's tapleaf names the pool's own dWallet key, so it
|
|
1327
|
+
* is already under pool custody and is recorded as a pool UTXO directly. The
|
|
1328
|
+
* SPV-verified transaction must therefore BE the deposit — `depositTxSize` is 0
|
|
1329
|
+
* and `depositTxid` defaults to `sweepTxid`. The receipt PDA is seeded
|
|
1324
1330
|
* `["deposit_receipt", txid, vout]`, so pass `depositVout` to
|
|
1325
1331
|
* `deriveDepositReceiptPDA` for this flow.
|
|
1326
1332
|
*
|
|
@@ -1329,9 +1335,10 @@ export function buildCompleteDepositInstructionData(params) {
|
|
|
1329
1335
|
* + note_public_key(32) + deposit_vout(u32 LE) = 149 bytes
|
|
1330
1336
|
*/
|
|
1331
1337
|
export function buildVerifyDepositInstructionData(params) {
|
|
1338
|
+
const depositTxid = params.depositTxid ?? params.sweepTxid;
|
|
1332
1339
|
for (const [name, value] of [
|
|
1333
1340
|
["sweepTxid", params.sweepTxid],
|
|
1334
|
-
["depositTxid",
|
|
1341
|
+
["depositTxid", depositTxid],
|
|
1335
1342
|
["ephemeralPubkey", params.ephemeralPubkey],
|
|
1336
1343
|
["notePublicKey", params.notePublicKey],
|
|
1337
1344
|
]) {
|
|
@@ -1339,8 +1346,11 @@ export function buildVerifyDepositInstructionData(params) {
|
|
|
1339
1346
|
throw new Error(`${name} must be 32 bytes, got ${value.length}`);
|
|
1340
1347
|
}
|
|
1341
1348
|
}
|
|
1342
|
-
if (params.depositTxSize
|
|
1343
|
-
throw new Error("verify_deposit
|
|
1349
|
+
if (params.depositTxSize) {
|
|
1350
|
+
throw new Error("verify_deposit takes no second transaction: depositTxSize must be 0");
|
|
1351
|
+
}
|
|
1352
|
+
if (toHex(depositTxid) !== toHex(params.sweepTxid)) {
|
|
1353
|
+
throw new Error("verify_deposit proves the deposit itself: depositTxid must equal sweepTxid");
|
|
1344
1354
|
}
|
|
1345
1355
|
const data = new Uint8Array(149);
|
|
1346
1356
|
const view = new DataView(data.buffer);
|
|
@@ -1352,9 +1362,9 @@ export function buildVerifyDepositInstructionData(params) {
|
|
|
1352
1362
|
offset += 8;
|
|
1353
1363
|
view.setUint32(offset, params.sweepTxSize, true);
|
|
1354
1364
|
offset += 4;
|
|
1355
|
-
view.setUint32(offset,
|
|
1356
|
-
offset += 4;
|
|
1357
|
-
data.set(
|
|
1365
|
+
view.setUint32(offset, 0, true);
|
|
1366
|
+
offset += 4; // no second transaction
|
|
1367
|
+
data.set(depositTxid, offset);
|
|
1358
1368
|
offset += 32;
|
|
1359
1369
|
data.set(params.ephemeralPubkey, offset);
|
|
1360
1370
|
offset += 32;
|
|
@@ -1364,6 +1374,26 @@ export function buildVerifyDepositInstructionData(params) {
|
|
|
1364
1374
|
offset += 4;
|
|
1365
1375
|
return data;
|
|
1366
1376
|
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Build utxopia verify_deposit_permissioned instruction data (disc=26).
|
|
1379
|
+
*
|
|
1380
|
+
* `buildVerifyDepositInstructionData`'s payload with a different discriminator
|
|
1381
|
+
* and an auditor ciphertext appended. The tapleaf already tells the pools apart —
|
|
1382
|
+
* each carries its own Ika custody key — but that is not the same as clearing the
|
|
1383
|
+
* pool's policy, which is what a permissioned pool exists for.
|
|
1384
|
+
*
|
|
1385
|
+
* The one-time PolicyApproval is bound to the WHOLE payload, ciphertext included,
|
|
1386
|
+
* so these exact bytes must be the ones approved.
|
|
1387
|
+
*/
|
|
1388
|
+
export function buildVerifyDepositPermissionedInstructionData(params) {
|
|
1389
|
+
const base = buildVerifyDepositInstructionData(params);
|
|
1390
|
+
const ciphertext = params.auditorCiphertext ?? new Uint8Array(0);
|
|
1391
|
+
const data = new Uint8Array(base.length + ciphertext.length);
|
|
1392
|
+
data.set(base, 0);
|
|
1393
|
+
data.set(ciphertext, base.length);
|
|
1394
|
+
data[0] = PERMISSIONED_DISC.VERIFY_DEPOSIT_PERMISSIONED;
|
|
1395
|
+
return data;
|
|
1396
|
+
}
|
|
1367
1397
|
// =============================================================================
|
|
1368
1398
|
// UTXOpia Set Pool Config (disc=2)
|
|
1369
1399
|
// =============================================================================
|
|
@@ -127,7 +127,7 @@ export interface OnChainStealthAnnouncement {
|
|
|
127
127
|
* 6. commitment = Poseidon(npk, tokenId, amount)
|
|
128
128
|
* 7. encryptedAmount = amount XOR sha256(sharedSecret)[0..8]
|
|
129
129
|
*/
|
|
130
|
-
export declare function createStealthDeposit(recipientMeta: StealthMetaAddress, amountSats: bigint, tokenId: bigint): Promise<StealthDeposit>;
|
|
130
|
+
export declare function createStealthDeposit(recipientMeta: StealthMetaAddress, amountSats: bigint, tokenId: bigint, outgoing?: OutgoingRecoveryMaterial): Promise<StealthDeposit>;
|
|
131
131
|
/**
|
|
132
132
|
* Extended stealth output data including the derived stealth pub key
|
|
133
133
|
*/
|
|
@@ -200,6 +200,86 @@ export declare function createNonInteractiveDeposit(recipientMeta: StealthMetaAd
|
|
|
200
200
|
* credits the note from that transaction.
|
|
201
201
|
*/
|
|
202
202
|
export declare function createDirectVaultDeposit(recipientMeta: StealthMetaAddress, vaultXOnlyPubkey: Uint8Array, network?: "mainnet" | "testnet" | "regtest", opReturnContext?: DepositOpReturnContext): Promise<NonInteractiveDepositResult>;
|
|
203
|
+
/**
|
|
204
|
+
* The delegable half of deposit recovery.
|
|
205
|
+
*
|
|
206
|
+
* Deposit addresses are derived from this node, so whoever holds it can rebuild
|
|
207
|
+
* every address the owner was ever handed, and with it every tapleaf and control
|
|
208
|
+
* block needed to spend those coins. It grants no spend authority over notes:
|
|
209
|
+
* that needs the spending key and the nullifying key, neither of which is
|
|
210
|
+
* derivable from here.
|
|
211
|
+
*
|
|
212
|
+
* Derived from the viewing key rather than the master seed on purpose. It means
|
|
213
|
+
* an owner can hand a chosen party — another device, a custodian, the pool
|
|
214
|
+
* operator — the ability to recover their BTC without handing over the ability
|
|
215
|
+
* to spend their notes. Same split Fluidkey uses for its ephemeral key nodes.
|
|
216
|
+
*/
|
|
217
|
+
export declare function depositViewingNode(viewingPrivKey: Uint8Array): Uint8Array;
|
|
218
|
+
/**
|
|
219
|
+
* The delegable half of *outgoing* history.
|
|
220
|
+
*
|
|
221
|
+
* An announcement is encrypted to the recipient's viewing key, and the sender
|
|
222
|
+
* discards the ephemeral private key — so today a sender cannot rediscover what
|
|
223
|
+
* they paid out. Their history exists only in local storage. Indexing the
|
|
224
|
+
* ephemeral key off this node makes every outgoing payment recomputable from
|
|
225
|
+
* keys alone. Zcash calls the equivalent an outgoing viewing key.
|
|
226
|
+
*
|
|
227
|
+
* Deliberately a different node from `depositViewingNode`. The two authorise
|
|
228
|
+
* different things, and one should not smuggle in the other:
|
|
229
|
+
*
|
|
230
|
+
* - deposit node → "you can recover my BTC"
|
|
231
|
+
* - outgoing node → "you can see who I paid"
|
|
232
|
+
*/
|
|
233
|
+
export declare function outgoingViewingNode(viewingPrivKey: Uint8Array): Uint8Array;
|
|
234
|
+
/** Which outgoing payment an ephemeral key belongs to. */
|
|
235
|
+
export interface OutgoingRecoveryMaterial {
|
|
236
|
+
/** From `outgoingViewingNode(viewingPrivKey)`. */
|
|
237
|
+
outgoingNode: Uint8Array;
|
|
238
|
+
/** Monotonic per-sender counter. One payment per index. */
|
|
239
|
+
sendIndex: number;
|
|
240
|
+
}
|
|
241
|
+
/** `sha256(domain || outgoingNode || sendIndex)` → Ed25519 ephemeral keypair. */
|
|
242
|
+
export declare function outgoingEphemeralKeyPair(outgoing: OutgoingRecoveryMaterial): {
|
|
243
|
+
privKey: Uint8Array;
|
|
244
|
+
pubKey: Uint8Array;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Recover the next unused send index by walking 0 upward against the ephemeral
|
|
248
|
+
* pubkeys already on chain.
|
|
249
|
+
*
|
|
250
|
+
* An index is "used" when some announcement carries the ephemeral pubkey it
|
|
251
|
+
* derives. Scanning stops after `gapLimit` consecutive misses, because a
|
|
252
|
+
* derived-but-never-broadcast payment leaves a hole — an abandoned or failed
|
|
253
|
+
* transaction — and stopping at the first hole would hand back an index that is
|
|
254
|
+
* already spoken for.
|
|
255
|
+
*
|
|
256
|
+
* This is a FLOOR, not the live counter. A payment broadcast but not yet indexed
|
|
257
|
+
* is invisible here, so a sender that keeps local state must take
|
|
258
|
+
* `max(localCounter, findNextSendIndex(...))`. Reusing an index re-derives the
|
|
259
|
+
* same ephemeral key, and to the same recipient that means the same note
|
|
260
|
+
* commitment twice.
|
|
261
|
+
*/
|
|
262
|
+
export declare function findNextSendIndex(outgoingNode: Uint8Array, seenEphemeralPubs: Iterable<Uint8Array>, gapLimit?: number): number;
|
|
263
|
+
/**
|
|
264
|
+
* What makes a deposit address reconstructable.
|
|
265
|
+
*
|
|
266
|
+
* The ephemeral key is NOT random. A deposit address commits to it via the
|
|
267
|
+
* tapleaf, and the key path is a NUMS point, so an address whose ephemeral key
|
|
268
|
+
* is lost is an address nobody — not the owner, not the pool — can ever spend.
|
|
269
|
+
* Indexing it off the viewing node makes that node a complete backup: walk
|
|
270
|
+
* `depositIndex` upward and every address comes back.
|
|
271
|
+
*/
|
|
272
|
+
export interface DepositRecoveryMaterial {
|
|
273
|
+
/** From `depositViewingNode(viewingPrivKey)`. */
|
|
274
|
+
viewingNode: Uint8Array;
|
|
275
|
+
/** Monotonic per-owner counter. One address per index. */
|
|
276
|
+
depositIndex: number;
|
|
277
|
+
}
|
|
278
|
+
/** `sha256(domain || viewingNode || depositIndex)` → Ed25519 ephemeral keypair. */
|
|
279
|
+
export declare function depositEphemeralKeyPair(recovery: DepositRecoveryMaterial): {
|
|
280
|
+
privKey: Uint8Array;
|
|
281
|
+
pubKey: Uint8Array;
|
|
282
|
+
};
|
|
203
283
|
/** A deposit whose address alone binds the note keys — no OP_RETURN. */
|
|
204
284
|
export interface TweakDepositResult {
|
|
205
285
|
/** Taproot address to send BTC to */
|
|
@@ -210,8 +290,14 @@ export interface TweakDepositResult {
|
|
|
210
290
|
npk: Uint8Array;
|
|
211
291
|
/** 32-byte Ed25519 ephemeral public key */
|
|
212
292
|
ephemeralPub: Uint8Array;
|
|
213
|
-
/** sha256(npk || ephemeralPub) — the commitment
|
|
293
|
+
/** sha256(npk || ephemeralPub) — the commitment carried in the tapleaf */
|
|
214
294
|
tweakCommitment: Uint8Array;
|
|
295
|
+
/** The tapleaf: `<commitment> OP_DROP <ika_xonly> OP_CHECKSIG` */
|
|
296
|
+
leafScript: Uint8Array;
|
|
297
|
+
/** Its BIP-341 tapleaf hash, which is also the merkle root (single leaf) */
|
|
298
|
+
leafHash: Uint8Array;
|
|
299
|
+
/** Script-path witness is `[signature, leafScript, controlBlock]` */
|
|
300
|
+
controlBlock: Uint8Array;
|
|
215
301
|
}
|
|
216
302
|
/**
|
|
217
303
|
* Create a deposit for the OP_RETURN-free flow (`verify_deposit`, disc 25).
|
|
@@ -219,15 +305,24 @@ export interface TweakDepositResult {
|
|
|
219
305
|
* The transaction carries nothing but a payment, so anything that can send to a
|
|
220
306
|
* P2TR address can fund it — a hardware wallet, an exchange withdrawal, a faucet.
|
|
221
307
|
* The note keys are recovered from instruction data at completion time and proven
|
|
222
|
-
* against this address's
|
|
223
|
-
* different address that the funding transaction never paid.
|
|
308
|
+
* against this address's tapleaf, so substituting either key derives a different
|
|
309
|
+
* leaf, and so a different address that the funding transaction never paid.
|
|
310
|
+
*
|
|
311
|
+
* The address is spendable only by `vaultXOnlyPubkey` via the script path; its
|
|
312
|
+
* key path is a NUMS point. That keeps the deposit under Ika custody from the
|
|
313
|
+
* moment it confirms, and it is also the only shape Ika can sign for — its MPC
|
|
314
|
+
* cannot produce a signature for a tweaked key.
|
|
315
|
+
*
|
|
316
|
+
* `recovery` is not optional on purpose. The address commits to the ephemeral
|
|
317
|
+
* key and the key path is unspendable, so a random ephemeral key that is later
|
|
318
|
+
* lost burns the coins outright. Indexing it off the viewing node means that
|
|
319
|
+
* node is the backup — and it can be delegated without granting spend authority.
|
|
224
320
|
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* so an unregistered address is one nobody is watching.
|
|
321
|
+
* Register the address with the tracker BEFORE any coins are sent — a deposit
|
|
322
|
+
* with no OP_RETURN is invisible to block scanning, so an unregistered address
|
|
323
|
+
* is one nobody is watching.
|
|
229
324
|
*/
|
|
230
|
-
export declare function createTweakDeposit(recipientMeta: StealthMetaAddress, vaultXOnlyPubkey: Uint8Array, network?: "mainnet" | "testnet" | "regtest"): Promise<TweakDepositResult>;
|
|
325
|
+
export declare function createTweakDeposit(recipientMeta: StealthMetaAddress, vaultXOnlyPubkey: Uint8Array, recovery: DepositRecoveryMaterial, network?: "mainnet" | "testnet" | "regtest"): Promise<TweakDepositResult>;
|
|
231
326
|
/**
|
|
232
327
|
* Create a non-interactive deposit using the current SDK config.
|
|
233
328
|
*
|
|
@@ -42,7 +42,7 @@ function isKnownAnnouncementType(value) {
|
|
|
42
42
|
// ========== Imports ==========
|
|
43
43
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
44
44
|
import { bigintToBytes, bytesToBigint, bytesToHex, hexToBytes, babyJubMul, babyJubAdd, babyJubCompress, babyJubDecompress, BABYJUB_BASE8, BABYJUB_ORDER, scalarFromBytes, } from "./crypto";
|
|
45
|
-
import { ed25519GenerateKeyPair, x25519Ecdh, encryptAmountEd25519, decryptAmountEd25519, } from "./crypto-ed25519";
|
|
45
|
+
import { ed25519GenerateKeyPair, ed25519KeyPairFromMaterial, x25519Ecdh, encryptAmountEd25519, decryptAmountEd25519, } from "./crypto-ed25519";
|
|
46
46
|
import { deriveKeysFromWallet } from "./keys";
|
|
47
47
|
import { computeMPKSync, computeNPKSync, computeJoinSplitCommitmentSync, computeJoinSplitNullifierSync, } from "./poseidon";
|
|
48
48
|
import { getConfig } from "./config";
|
|
@@ -124,11 +124,17 @@ function deriveStealthPrivKey(spendingPriv, sharedSecret) {
|
|
|
124
124
|
* 6. commitment = Poseidon(npk, tokenId, amount)
|
|
125
125
|
* 7. encryptedAmount = amount XOR sha256(sharedSecret)[0..8]
|
|
126
126
|
*/
|
|
127
|
-
export async function createStealthDeposit(recipientMeta, amountSats, tokenId) {
|
|
127
|
+
export async function createStealthDeposit(recipientMeta, amountSats, tokenId, outgoing) {
|
|
128
128
|
// Only viewingPubKey + mpk needed (spendingPubKey not used by sender)
|
|
129
129
|
const viewingPubKey = new Uint8Array(recipientMeta.viewingPubKey);
|
|
130
|
-
//
|
|
131
|
-
|
|
130
|
+
// Indexed off the sender's outgoing node when supplied, so they can recompute
|
|
131
|
+
// this payment later; random otherwise, which works but leaves no trace the
|
|
132
|
+
// sender can recover. Optional rather than required because the cost of
|
|
133
|
+
// omitting it is a lost record, not lost funds — unlike a deposit address,
|
|
134
|
+
// where the ephemeral key IS the only way to ever spend the coins.
|
|
135
|
+
const ephemeral = outgoing
|
|
136
|
+
? outgoingEphemeralKeyPair(outgoing)
|
|
137
|
+
: ed25519GenerateKeyPair();
|
|
132
138
|
// X25519 ECDH: shared secret
|
|
133
139
|
const sharedSecret = x25519Ecdh(ephemeral.privKey, viewingPubKey);
|
|
134
140
|
// Derive stealth scalar as the random value for NPK
|
|
@@ -248,40 +254,166 @@ export async function createDirectVaultDeposit(recipientMeta, vaultXOnlyPubkey,
|
|
|
248
254
|
ephemeralPub,
|
|
249
255
|
};
|
|
250
256
|
}
|
|
257
|
+
const DEPOSIT_VIEWING_NODE_DOMAIN = new TextEncoder().encode("utxopia:deposit-viewing-node:v1");
|
|
258
|
+
const DEPOSIT_EPHEMERAL_DOMAIN = new TextEncoder().encode("utxopia:deposit-ephemeral:v1");
|
|
259
|
+
/**
|
|
260
|
+
* The delegable half of deposit recovery.
|
|
261
|
+
*
|
|
262
|
+
* Deposit addresses are derived from this node, so whoever holds it can rebuild
|
|
263
|
+
* every address the owner was ever handed, and with it every tapleaf and control
|
|
264
|
+
* block needed to spend those coins. It grants no spend authority over notes:
|
|
265
|
+
* that needs the spending key and the nullifying key, neither of which is
|
|
266
|
+
* derivable from here.
|
|
267
|
+
*
|
|
268
|
+
* Derived from the viewing key rather than the master seed on purpose. It means
|
|
269
|
+
* an owner can hand a chosen party — another device, a custodian, the pool
|
|
270
|
+
* operator — the ability to recover their BTC without handing over the ability
|
|
271
|
+
* to spend their notes. Same split Fluidkey uses for its ephemeral key nodes.
|
|
272
|
+
*/
|
|
273
|
+
export function depositViewingNode(viewingPrivKey) {
|
|
274
|
+
if (viewingPrivKey.length === 0) {
|
|
275
|
+
throw new Error("viewingPrivKey must not be empty");
|
|
276
|
+
}
|
|
277
|
+
const material = new Uint8Array(DEPOSIT_VIEWING_NODE_DOMAIN.length + viewingPrivKey.length);
|
|
278
|
+
material.set(DEPOSIT_VIEWING_NODE_DOMAIN, 0);
|
|
279
|
+
material.set(viewingPrivKey, DEPOSIT_VIEWING_NODE_DOMAIN.length);
|
|
280
|
+
return sha256(material);
|
|
281
|
+
}
|
|
282
|
+
const OUTGOING_VIEWING_NODE_DOMAIN = new TextEncoder().encode("utxopia:outgoing-viewing-node:v1");
|
|
283
|
+
const OUTGOING_EPHEMERAL_DOMAIN = new TextEncoder().encode("utxopia:outgoing-ephemeral:v1");
|
|
284
|
+
/**
|
|
285
|
+
* The delegable half of *outgoing* history.
|
|
286
|
+
*
|
|
287
|
+
* An announcement is encrypted to the recipient's viewing key, and the sender
|
|
288
|
+
* discards the ephemeral private key — so today a sender cannot rediscover what
|
|
289
|
+
* they paid out. Their history exists only in local storage. Indexing the
|
|
290
|
+
* ephemeral key off this node makes every outgoing payment recomputable from
|
|
291
|
+
* keys alone. Zcash calls the equivalent an outgoing viewing key.
|
|
292
|
+
*
|
|
293
|
+
* Deliberately a different node from `depositViewingNode`. The two authorise
|
|
294
|
+
* different things, and one should not smuggle in the other:
|
|
295
|
+
*
|
|
296
|
+
* - deposit node → "you can recover my BTC"
|
|
297
|
+
* - outgoing node → "you can see who I paid"
|
|
298
|
+
*/
|
|
299
|
+
export function outgoingViewingNode(viewingPrivKey) {
|
|
300
|
+
if (viewingPrivKey.length === 0) {
|
|
301
|
+
throw new Error("viewingPrivKey must not be empty");
|
|
302
|
+
}
|
|
303
|
+
const material = new Uint8Array(OUTGOING_VIEWING_NODE_DOMAIN.length + viewingPrivKey.length);
|
|
304
|
+
material.set(OUTGOING_VIEWING_NODE_DOMAIN, 0);
|
|
305
|
+
material.set(viewingPrivKey, OUTGOING_VIEWING_NODE_DOMAIN.length);
|
|
306
|
+
return sha256(material);
|
|
307
|
+
}
|
|
308
|
+
/** `sha256(domain || outgoingNode || sendIndex)` → Ed25519 ephemeral keypair. */
|
|
309
|
+
export function outgoingEphemeralKeyPair(outgoing) {
|
|
310
|
+
const { outgoingNode, sendIndex } = outgoing;
|
|
311
|
+
if (outgoingNode.length === 0) {
|
|
312
|
+
throw new Error("outgoingNode must not be empty");
|
|
313
|
+
}
|
|
314
|
+
if (!Number.isInteger(sendIndex) || sendIndex < 0) {
|
|
315
|
+
throw new Error(`invalid sendIndex: ${sendIndex}`);
|
|
316
|
+
}
|
|
317
|
+
const material = new Uint8Array(OUTGOING_EPHEMERAL_DOMAIN.length + outgoingNode.length + 4);
|
|
318
|
+
material.set(OUTGOING_EPHEMERAL_DOMAIN, 0);
|
|
319
|
+
material.set(outgoingNode, OUTGOING_EPHEMERAL_DOMAIN.length);
|
|
320
|
+
new DataView(material.buffer).setUint32(OUTGOING_EPHEMERAL_DOMAIN.length + outgoingNode.length, sendIndex, true);
|
|
321
|
+
return ed25519KeyPairFromMaterial(material);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Recover the next unused send index by walking 0 upward against the ephemeral
|
|
325
|
+
* pubkeys already on chain.
|
|
326
|
+
*
|
|
327
|
+
* An index is "used" when some announcement carries the ephemeral pubkey it
|
|
328
|
+
* derives. Scanning stops after `gapLimit` consecutive misses, because a
|
|
329
|
+
* derived-but-never-broadcast payment leaves a hole — an abandoned or failed
|
|
330
|
+
* transaction — and stopping at the first hole would hand back an index that is
|
|
331
|
+
* already spoken for.
|
|
332
|
+
*
|
|
333
|
+
* This is a FLOOR, not the live counter. A payment broadcast but not yet indexed
|
|
334
|
+
* is invisible here, so a sender that keeps local state must take
|
|
335
|
+
* `max(localCounter, findNextSendIndex(...))`. Reusing an index re-derives the
|
|
336
|
+
* same ephemeral key, and to the same recipient that means the same note
|
|
337
|
+
* commitment twice.
|
|
338
|
+
*/
|
|
339
|
+
export function findNextSendIndex(outgoingNode, seenEphemeralPubs, gapLimit = 20) {
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
for (const pub of seenEphemeralPubs)
|
|
342
|
+
seen.add(bytesToHex(pub));
|
|
343
|
+
let highestUsed = -1;
|
|
344
|
+
let misses = 0;
|
|
345
|
+
for (let index = 0; misses <= gapLimit; index++) {
|
|
346
|
+
if (seen.has(bytesToHex(outgoingEphemeralKeyPair({ outgoingNode, sendIndex: index }).pubKey))) {
|
|
347
|
+
highestUsed = index;
|
|
348
|
+
misses = 0;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
misses++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return highestUsed + 1;
|
|
355
|
+
}
|
|
356
|
+
/** `sha256(domain || viewingNode || depositIndex)` → Ed25519 ephemeral keypair. */
|
|
357
|
+
export function depositEphemeralKeyPair(recovery) {
|
|
358
|
+
const { viewingNode, depositIndex } = recovery;
|
|
359
|
+
if (viewingNode.length === 0) {
|
|
360
|
+
throw new Error("deposit viewingNode must not be empty");
|
|
361
|
+
}
|
|
362
|
+
if (!Number.isInteger(depositIndex) || depositIndex < 0) {
|
|
363
|
+
throw new Error(`invalid depositIndex: ${depositIndex}`);
|
|
364
|
+
}
|
|
365
|
+
const material = new Uint8Array(DEPOSIT_EPHEMERAL_DOMAIN.length + viewingNode.length + 4);
|
|
366
|
+
material.set(DEPOSIT_EPHEMERAL_DOMAIN, 0);
|
|
367
|
+
material.set(viewingNode, DEPOSIT_EPHEMERAL_DOMAIN.length);
|
|
368
|
+
new DataView(material.buffer).setUint32(DEPOSIT_EPHEMERAL_DOMAIN.length + viewingNode.length, depositIndex, true);
|
|
369
|
+
return ed25519KeyPairFromMaterial(material);
|
|
370
|
+
}
|
|
251
371
|
/**
|
|
252
372
|
* Create a deposit for the OP_RETURN-free flow (`verify_deposit`, disc 25).
|
|
253
373
|
*
|
|
254
374
|
* The transaction carries nothing but a payment, so anything that can send to a
|
|
255
375
|
* P2TR address can fund it — a hardware wallet, an exchange withdrawal, a faucet.
|
|
256
376
|
* The note keys are recovered from instruction data at completion time and proven
|
|
257
|
-
* against this address's
|
|
258
|
-
* different address that the funding transaction never paid.
|
|
377
|
+
* against this address's tapleaf, so substituting either key derives a different
|
|
378
|
+
* leaf, and so a different address that the funding transaction never paid.
|
|
259
379
|
*
|
|
260
|
-
*
|
|
261
|
-
* is
|
|
262
|
-
*
|
|
263
|
-
*
|
|
380
|
+
* The address is spendable only by `vaultXOnlyPubkey` via the script path; its
|
|
381
|
+
* key path is a NUMS point. That keeps the deposit under Ika custody from the
|
|
382
|
+
* moment it confirms, and it is also the only shape Ika can sign for — its MPC
|
|
383
|
+
* cannot produce a signature for a tweaked key.
|
|
384
|
+
*
|
|
385
|
+
* `recovery` is not optional on purpose. The address commits to the ephemeral
|
|
386
|
+
* key and the key path is unspendable, so a random ephemeral key that is later
|
|
387
|
+
* lost burns the coins outright. Indexing it off the viewing node means that
|
|
388
|
+
* node is the backup — and it can be delegated without granting spend authority.
|
|
389
|
+
*
|
|
390
|
+
* Register the address with the tracker BEFORE any coins are sent — a deposit
|
|
391
|
+
* with no OP_RETURN is invisible to block scanning, so an unregistered address
|
|
392
|
+
* is one nobody is watching.
|
|
264
393
|
*/
|
|
265
|
-
export async function createTweakDeposit(recipientMeta, vaultXOnlyPubkey, network = "testnet") {
|
|
394
|
+
export async function createTweakDeposit(recipientMeta, vaultXOnlyPubkey, recovery, network = "testnet") {
|
|
266
395
|
if (vaultXOnlyPubkey.length !== 32) {
|
|
267
396
|
throw new Error("vaultXOnlyPubkey must be 32 bytes");
|
|
268
397
|
}
|
|
269
398
|
const viewingPubKey = new Uint8Array(recipientMeta.viewingPubKey);
|
|
270
|
-
const ephemeral =
|
|
399
|
+
const ephemeral = depositEphemeralKeyPair(recovery);
|
|
271
400
|
const sharedSecret = x25519Ecdh(ephemeral.privKey, viewingPubKey);
|
|
272
401
|
const stealthScalar = deriveStealthScalar(sharedSecret);
|
|
273
402
|
const recipientMPK = bytesToBigint(recipientMeta.mpk);
|
|
274
403
|
const npk = bigintToBytes(computeNPKSync(recipientMPK, stealthScalar));
|
|
275
404
|
const ephemeralPub = new Uint8Array(ephemeral.pubKey);
|
|
276
|
-
const { depositTweakCommitment,
|
|
405
|
+
const { depositTweakCommitment, deriveDepositAddress } = await import("./taproot");
|
|
277
406
|
const tweakCommitment = depositTweakCommitment(npk, ephemeralPub);
|
|
278
|
-
const { address, outputKey } =
|
|
407
|
+
const { address, outputKey, leafScript, leafHash, controlBlock } = deriveDepositAddress(tweakCommitment, vaultXOnlyPubkey, network);
|
|
279
408
|
return {
|
|
280
409
|
btcAddress: address,
|
|
281
410
|
depositOutputKey: outputKey,
|
|
282
411
|
npk,
|
|
283
412
|
ephemeralPub,
|
|
284
413
|
tweakCommitment,
|
|
414
|
+
leafScript,
|
|
415
|
+
leafHash,
|
|
416
|
+
controlBlock,
|
|
285
417
|
};
|
|
286
418
|
}
|
|
287
419
|
/**
|
|
@@ -22,18 +22,55 @@ export declare function deriveTaprootAddress(commitment: Uint8Array, network?: "
|
|
|
22
22
|
address: string;
|
|
23
23
|
outputKey: Uint8Array;
|
|
24
24
|
tweak: Uint8Array;
|
|
25
|
+
/** y-parity of the output key — the low bit of a script-path control block. */
|
|
26
|
+
parity: number;
|
|
25
27
|
};
|
|
26
28
|
/**
|
|
27
|
-
*
|
|
29
|
+
* Per-deposit commitment carried in a `verify_deposit` (disc 25) address's tapleaf.
|
|
28
30
|
*
|
|
29
31
|
* Both keys are hashed in. Binding the note key alone would leave the ephemeral
|
|
30
32
|
* pubkey caller-chosen on the Solana side: the credited amount and owner would
|
|
31
33
|
* still be right, but a substituted ephemeral key makes the stealth announcement
|
|
32
34
|
* undecryptable and the recipient never finds their note.
|
|
33
35
|
*
|
|
34
|
-
* Feed the result to `
|
|
36
|
+
* Feed the result to `deriveDepositAddress`.
|
|
35
37
|
*/
|
|
36
38
|
export declare function depositTweakCommitment(notePublicKey: Uint8Array, ephemeralPubkey: Uint8Array): Uint8Array;
|
|
39
|
+
/**
|
|
40
|
+
* BIP-341's suggested NUMS point, used as the deposit address's internal key.
|
|
41
|
+
*
|
|
42
|
+
* Nobody knows its discrete log, so the key path is unspendable and custody
|
|
43
|
+
* rests entirely on the script path. That is deliberate: Ika's MPC cannot sign
|
|
44
|
+
* for a tweaked key, so an address whose internal key were the dWallet key plus
|
|
45
|
+
* a per-deposit tweak would be unspendable by the custodian meant to sweep it.
|
|
46
|
+
*/
|
|
47
|
+
export declare const DEPOSIT_NUMS_INTERNAL_KEY: Uint8Array<ArrayBufferLike>;
|
|
48
|
+
/**
|
|
49
|
+
* The single tapleaf a deposit address commits to:
|
|
50
|
+
*
|
|
51
|
+
* ```text
|
|
52
|
+
* <commitment> OP_DROP <ika_xonly> OP_CHECKSIG
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* The commitment rides in the script purely to make the leaf — and so the
|
|
56
|
+
* address — unique per deposit; `OP_DROP` discards it at spend time. Only the
|
|
57
|
+
* dWallet key can satisfy the `OP_CHECKSIG`, and it signs untweaked.
|
|
58
|
+
*/
|
|
59
|
+
export declare function depositLeafScript(commitment: Uint8Array, ikaXOnlyPubkey: Uint8Array): Uint8Array;
|
|
60
|
+
/**
|
|
61
|
+
* Derive a deposit address bound to `commitment`, spendable only by the pool's
|
|
62
|
+
* Ika dWallet via the script path.
|
|
63
|
+
*
|
|
64
|
+
* Returns everything the sweeper needs to spend it: a script-path witness is
|
|
65
|
+
* `[signature, leafScript, controlBlock]`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function deriveDepositAddress(commitment: Uint8Array, ikaXOnlyPubkey: Uint8Array, network?: "mainnet" | "testnet" | "regtest"): {
|
|
68
|
+
address: string;
|
|
69
|
+
outputKey: Uint8Array;
|
|
70
|
+
leafScript: Uint8Array;
|
|
71
|
+
leafHash: Uint8Array;
|
|
72
|
+
controlBlock: Uint8Array;
|
|
73
|
+
};
|
|
37
74
|
/**
|
|
38
75
|
* Verify that a Taproot address is correctly derived from a commitment
|
|
39
76
|
*
|