@utxopia/sdk 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +171 -0
- package/package.json +108 -0
- package/packages/btc-client/src/esplora-client.ts +153 -0
- package/packages/btc-client/src/index.ts +3 -0
- package/packages/btc-client/src/op-return.ts +93 -0
- package/packages/btc-client/src/types.ts +112 -0
- package/packages/sdk/README.md +277 -0
- package/packages/sdk/dist/announcement-client.d.ts +64 -0
- package/packages/sdk/dist/announcement-client.js +337 -0
- package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
- package/packages/sdk/dist/auditor-ciphertext.js +135 -0
- package/packages/sdk/dist/auditor.d.ts +115 -0
- package/packages/sdk/dist/auditor.js +292 -0
- package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
- package/packages/sdk/dist/bitcoin/ika.js +74 -0
- package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
- package/packages/sdk/dist/bitcoin/index.js +1 -0
- package/packages/sdk/dist/bound-params.d.ts +96 -0
- package/packages/sdk/dist/bound-params.js +249 -0
- package/packages/sdk/dist/chadbuffer.d.ts +124 -0
- package/packages/sdk/dist/chadbuffer.js +405 -0
- package/packages/sdk/dist/claim-link.d.ts +30 -0
- package/packages/sdk/dist/claim-link.js +50 -0
- package/packages/sdk/dist/client.d.ts +233 -0
- package/packages/sdk/dist/client.js +416 -0
- package/packages/sdk/dist/commitment-tree.d.ts +268 -0
- package/packages/sdk/dist/commitment-tree.js +564 -0
- package/packages/sdk/dist/config.d.ts +177 -0
- package/packages/sdk/dist/config.js +426 -0
- package/packages/sdk/dist/core/esplora.d.ts +110 -0
- package/packages/sdk/dist/core/esplora.js +187 -0
- package/packages/sdk/dist/core/mempool.d.ts +60 -0
- package/packages/sdk/dist/core/mempool.js +107 -0
- package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
- package/packages/sdk/dist/crypto-babyjub.js +312 -0
- package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
- package/packages/sdk/dist/crypto-ed25519.js +235 -0
- package/packages/sdk/dist/crypto.d.ts +59 -0
- package/packages/sdk/dist/crypto.js +140 -0
- package/packages/sdk/dist/event-client.d.ts +64 -0
- package/packages/sdk/dist/event-client.js +165 -0
- package/packages/sdk/dist/events.d.ts +127 -0
- package/packages/sdk/dist/events.js +305 -0
- package/packages/sdk/dist/explorer.d.ts +86 -0
- package/packages/sdk/dist/explorer.js +197 -0
- package/packages/sdk/dist/index.d.ts +61 -0
- package/packages/sdk/dist/index.js +216 -0
- package/packages/sdk/dist/instructions.d.ts +1125 -0
- package/packages/sdk/dist/instructions.js +1760 -0
- package/packages/sdk/dist/keys.d.ts +467 -0
- package/packages/sdk/dist/keys.js +799 -0
- package/packages/sdk/dist/logger.d.ts +10 -0
- package/packages/sdk/dist/logger.js +40 -0
- package/packages/sdk/dist/magicblock.d.ts +90 -0
- package/packages/sdk/dist/magicblock.js +164 -0
- package/packages/sdk/dist/merkle.d.ts +82 -0
- package/packages/sdk/dist/merkle.js +141 -0
- package/packages/sdk/dist/note.d.ts +348 -0
- package/packages/sdk/dist/note.js +483 -0
- package/packages/sdk/dist/pda.d.ts +160 -0
- package/packages/sdk/dist/pda.js +384 -0
- package/packages/sdk/dist/pool-state.d.ts +100 -0
- package/packages/sdk/dist/pool-state.js +126 -0
- package/packages/sdk/dist/poseidon.d.ts +53 -0
- package/packages/sdk/dist/poseidon.js +136 -0
- package/packages/sdk/dist/prover/index.d.ts +10 -0
- package/packages/sdk/dist/prover/index.js +10 -0
- package/packages/sdk/dist/prover/mobile.d.ts +45 -0
- package/packages/sdk/dist/prover/mobile.js +227 -0
- package/packages/sdk/dist/prover/web.d.ts +136 -0
- package/packages/sdk/dist/prover/web.js +597 -0
- package/packages/sdk/dist/psbt.d.ts +73 -0
- package/packages/sdk/dist/psbt.js +202 -0
- package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
- package/packages/sdk/dist/selective-disclosure.js +172 -0
- package/packages/sdk/dist/sender-memo.d.ts +149 -0
- package/packages/sdk/dist/sender-memo.js +250 -0
- package/packages/sdk/dist/sns-resolver.d.ts +121 -0
- package/packages/sdk/dist/sns-resolver.js +229 -0
- package/packages/sdk/dist/solana/connection.d.ts +77 -0
- package/packages/sdk/dist/solana/connection.js +133 -0
- package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
- package/packages/sdk/dist/solana/priority-fee.js +137 -0
- package/packages/sdk/dist/stealth.d.ts +429 -0
- package/packages/sdk/dist/stealth.js +703 -0
- package/packages/sdk/dist/taproot.d.ts +172 -0
- package/packages/sdk/dist/taproot.js +494 -0
- package/packages/sdk/dist/token-registry.d.ts +96 -0
- package/packages/sdk/dist/token-registry.js +122 -0
- package/packages/sdk/dist/utils/encoding.d.ts +19 -0
- package/packages/sdk/dist/utils/encoding.js +29 -0
- package/packages/sdk/dist/vk-registry.d.ts +100 -0
- package/packages/sdk/dist/vk-registry.js +212 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taproot address utilities for UTXOpia
|
|
3
|
+
*
|
|
4
|
+
* Generates commitment-bound Taproot addresses following BIP-340/341.
|
|
5
|
+
* The deposit address is derived from the commitment, ensuring
|
|
6
|
+
* cryptographic binding between the BTC deposit and the claim.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Derive a Taproot address from a commitment
|
|
10
|
+
*
|
|
11
|
+
* Following BIP-341:
|
|
12
|
+
* tweak = H_TapTweak(internal_key || commitment)
|
|
13
|
+
* output_key = internal_key + tweak * G
|
|
14
|
+
* address = bech32m encode(output_key)
|
|
15
|
+
*
|
|
16
|
+
* @param commitment - 32-byte commitment hash
|
|
17
|
+
* @param network - 'mainnet' | 'testnet' | 'regtest'
|
|
18
|
+
* @param internalKey - Configured FROST/Ika custody key (x-only, 32 bytes; required)
|
|
19
|
+
* @returns Taproot address (bc1p... or tb1p...)
|
|
20
|
+
*/
|
|
21
|
+
export declare function deriveTaprootAddress(commitment: Uint8Array, network?: "mainnet" | "testnet" | "regtest", internalKey?: Uint8Array): {
|
|
22
|
+
address: string;
|
|
23
|
+
outputKey: Uint8Array;
|
|
24
|
+
tweak: Uint8Array;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Verify that a Taproot address is correctly derived from a commitment
|
|
28
|
+
*
|
|
29
|
+
* @param address - Taproot address to verify
|
|
30
|
+
* @param commitment - Expected commitment
|
|
31
|
+
* @param internalKey - Configured FROST/Ika custody key (x-only, 32 bytes; required)
|
|
32
|
+
* @returns true if address matches expected derivation
|
|
33
|
+
*/
|
|
34
|
+
export declare function verifyTaprootAddress(address: string, commitment: Uint8Array, internalKey?: Uint8Array): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Generate a P2TR (Pay-to-Taproot) script pubkey
|
|
37
|
+
*
|
|
38
|
+
* @param outputKey - 32-byte output key (x-only)
|
|
39
|
+
* @returns Script pubkey bytes (OP_1 <32-byte key>)
|
|
40
|
+
*/
|
|
41
|
+
export declare function createP2TRScriptPubkey(outputKey: Uint8Array): Uint8Array;
|
|
42
|
+
/**
|
|
43
|
+
* Parse P2TR script pubkey to extract output key
|
|
44
|
+
*
|
|
45
|
+
* @param scriptPubkey - Script pubkey bytes
|
|
46
|
+
* @returns Output key or null if not P2TR
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseP2TRScriptPubkey(scriptPubkey: Uint8Array): Uint8Array | null;
|
|
49
|
+
/**
|
|
50
|
+
* Validate a Bitcoin address format
|
|
51
|
+
*/
|
|
52
|
+
export declare function isValidBitcoinAddress(address: string): {
|
|
53
|
+
valid: boolean;
|
|
54
|
+
type: "p2pkh" | "p2sh" | "p2wpkh" | "p2wsh" | "p2tr" | "unknown";
|
|
55
|
+
network: "mainnet" | "testnet" | "unknown";
|
|
56
|
+
};
|
|
57
|
+
/** Destination chain encoded in the compact deposit OP_RETURN header. */
|
|
58
|
+
export declare const DEPOSIT_DESTINATION_CHAIN: {
|
|
59
|
+
readonly SOLANA: 1;
|
|
60
|
+
};
|
|
61
|
+
export type DepositDestinationChain = (typeof DEPOSIT_DESTINATION_CHAIN)[keyof typeof DEPOSIT_DESTINATION_CHAIN];
|
|
62
|
+
/** Bitcoin network encoded in the compact deposit OP_RETURN header. */
|
|
63
|
+
export declare const DEPOSIT_BITCOIN_NETWORK: {
|
|
64
|
+
readonly MAINNET: 0;
|
|
65
|
+
readonly TESTNET4: 2;
|
|
66
|
+
readonly REGTEST: 3;
|
|
67
|
+
};
|
|
68
|
+
export type DepositBitcoinNetwork = (typeof DEPOSIT_BITCOIN_NETWORK)[keyof typeof DEPOSIT_BITCOIN_NETWORK];
|
|
69
|
+
export declare const DEPOSIT_OP_RETURN_VERSION = 1;
|
|
70
|
+
export declare const DEPOSIT_POOL_TAG_SIZE = 8;
|
|
71
|
+
/** OP_RETURN payload size for deposit: header(1) + poolTag(8) + ephemeralPubkey(32) + notePublicKey(32). */
|
|
72
|
+
export declare const DEPOSIT_OP_RETURN_SIZE = 73;
|
|
73
|
+
export interface DepositOpReturnContext {
|
|
74
|
+
destinationChain: DepositDestinationChain;
|
|
75
|
+
bitcoinNetwork: DepositBitcoinNetwork;
|
|
76
|
+
poolTag: Uint8Array;
|
|
77
|
+
}
|
|
78
|
+
export interface ParsedDepositOpReturn extends DepositOpReturnContext {
|
|
79
|
+
version: number;
|
|
80
|
+
ephemeralPubkey: Uint8Array;
|
|
81
|
+
notePublicKey: Uint8Array;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build the compact deposit OP_RETURN payload for non-interactive stealth deposits.
|
|
85
|
+
*
|
|
86
|
+
* Layout:
|
|
87
|
+
* [0] header — version + destination chain + Bitcoin network
|
|
88
|
+
* [1..9) poolTag — destination deployment tag
|
|
89
|
+
* [9..41) ephemeralPubkey — Ed25519 public key
|
|
90
|
+
* [41..73) notePublicKey — Note public key (Poseidon hash)
|
|
91
|
+
*
|
|
92
|
+
* Amount is no longer embedded — the on-chain program reads it from the BTC output.
|
|
93
|
+
* The caller wraps this in an OP_RETURN script (0x6a + push opcode + payload).
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildDepositOpReturn(ephemeralPubkey: Uint8Array, notePublicKey: Uint8Array, context: DepositOpReturnContext): Uint8Array;
|
|
96
|
+
/**
|
|
97
|
+
* Parse the compact deposit OP_RETURN payload back into its constituent fields.
|
|
98
|
+
*
|
|
99
|
+
* @returns Parsed fields, or null if data is not exactly the expected size.
|
|
100
|
+
*/
|
|
101
|
+
export declare function parseDepositOpReturn(data: Uint8Array): ParsedDepositOpReturn | null;
|
|
102
|
+
export declare function encodeDepositOpReturnHeader(destinationChain: DepositDestinationChain, bitcoinNetwork: DepositBitcoinNetwork): number;
|
|
103
|
+
export declare function decodeDepositOpReturnHeader(header: number): {
|
|
104
|
+
version: number;
|
|
105
|
+
destinationChain: DepositDestinationChain;
|
|
106
|
+
bitcoinNetwork: DepositBitcoinNetwork;
|
|
107
|
+
} | null;
|
|
108
|
+
export declare function validateDepositOpReturnContext(context: DepositOpReturnContext): void;
|
|
109
|
+
export declare function computeDepositPoolTag(parts: Uint8Array[]): Uint8Array;
|
|
110
|
+
/**
|
|
111
|
+
* Create an OP_RETURN script from an arbitrary payload (up to 80 bytes).
|
|
112
|
+
*
|
|
113
|
+
* Format: OP_RETURN (0x6a) + OP_PUSHDATA (length byte) + payload
|
|
114
|
+
*/
|
|
115
|
+
export declare function createOpReturnScriptFromPayload(payload: Uint8Array): Uint8Array;
|
|
116
|
+
/**
|
|
117
|
+
* Build a refund script for time-locked user recovery.
|
|
118
|
+
*
|
|
119
|
+
* Script:
|
|
120
|
+
* <npk_32> OP_DROP <144> OP_CHECKSEQUENCEVERIFY OP_DROP <user_x_only_pubkey_32> OP_CHECKSIG
|
|
121
|
+
*
|
|
122
|
+
* @param npk - 32-byte note public key (commitment binding)
|
|
123
|
+
* @param userPubkey - 32-byte x-only public key for the refund path
|
|
124
|
+
* @returns Script bytes (73 bytes)
|
|
125
|
+
*/
|
|
126
|
+
export declare function buildRefundScript(npk: Uint8Array, userPubkey: Uint8Array): Uint8Array;
|
|
127
|
+
/**
|
|
128
|
+
* Compute a TapLeaf hash per BIP-341.
|
|
129
|
+
*
|
|
130
|
+
* TapLeaf = H_TapLeaf(leafVersion || compactSize(script.length) || script)
|
|
131
|
+
*
|
|
132
|
+
* @param script - The leaf script bytes
|
|
133
|
+
* @param leafVersion - Leaf version byte (default 0xc0)
|
|
134
|
+
* @returns 32-byte tagged hash
|
|
135
|
+
*/
|
|
136
|
+
export declare function computeTapLeafHash(script: Uint8Array, leafVersion?: number): Uint8Array;
|
|
137
|
+
/**
|
|
138
|
+
* Derive a Taproot address with a refund script path.
|
|
139
|
+
*
|
|
140
|
+
* The address commits to both the FROST group key (internal key) and a
|
|
141
|
+
* time-locked refund script that allows the user to reclaim funds after
|
|
142
|
+
* 144 blocks (~1 day) if the bridge fails to sweep.
|
|
143
|
+
*
|
|
144
|
+
* Taproot construction:
|
|
145
|
+
* - Internal key = FROST group key (x-only)
|
|
146
|
+
* - Single TapLeaf = refund script
|
|
147
|
+
* - Merkle root = TapLeaf hash (single leaf, no branching)
|
|
148
|
+
* - Tweak = H_TapTweak(internal_key || merkle_root)
|
|
149
|
+
* - Output key = internal_key + tweak * G
|
|
150
|
+
*
|
|
151
|
+
* @param npk - 32-byte note public key (embedded in refund script for binding)
|
|
152
|
+
* @param userRefundPubkey - 32-byte x-only pubkey for the refund spending path
|
|
153
|
+
* @param internalKey - 32-byte x-only FROST group public key
|
|
154
|
+
* @param network - Bitcoin network for address encoding
|
|
155
|
+
*/
|
|
156
|
+
export declare function deriveTaprootAddressWithRefund(npk: Uint8Array, userRefundPubkey: Uint8Array, internalKey: Uint8Array, network?: "mainnet" | "testnet" | "regtest"): {
|
|
157
|
+
address: string;
|
|
158
|
+
outputKey: Uint8Array;
|
|
159
|
+
merkleRoot: Uint8Array;
|
|
160
|
+
controlBlock: Uint8Array;
|
|
161
|
+
refundScript: Uint8Array;
|
|
162
|
+
tweak: Uint8Array;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Get the internal key used by UTXOpia
|
|
166
|
+
* In production, this would be the FROST threshold public key
|
|
167
|
+
*/
|
|
168
|
+
export declare function getInternalKey(): Uint8Array;
|
|
169
|
+
/**
|
|
170
|
+
* Set a custom internal key (for testing or custom deployments)
|
|
171
|
+
*/
|
|
172
|
+
export declare function createCustomInternalKey(key: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taproot address utilities for UTXOpia
|
|
3
|
+
*
|
|
4
|
+
* Generates commitment-bound Taproot addresses following BIP-340/341.
|
|
5
|
+
* The deposit address is derived from the commitment, ensuring
|
|
6
|
+
* cryptographic binding between the BTC deposit and the claim.
|
|
7
|
+
*/
|
|
8
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
9
|
+
import { taggedHash, hexToBytes, bytesToHex } from "./crypto";
|
|
10
|
+
import * as bech32 from "bech32";
|
|
11
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
12
|
+
// Never use the secp256k1 generator as a custody key. Its discrete log is
|
|
13
|
+
// public, so a key-path output derived from it is sweepable by anyone.
|
|
14
|
+
const UNSAFE_GENERATOR_INTERNAL_KEY_HEX = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; // secp256k1 generator x-coord
|
|
15
|
+
/**
|
|
16
|
+
* Derive a Taproot address from a commitment
|
|
17
|
+
*
|
|
18
|
+
* Following BIP-341:
|
|
19
|
+
* tweak = H_TapTweak(internal_key || commitment)
|
|
20
|
+
* output_key = internal_key + tweak * G
|
|
21
|
+
* address = bech32m encode(output_key)
|
|
22
|
+
*
|
|
23
|
+
* @param commitment - 32-byte commitment hash
|
|
24
|
+
* @param network - 'mainnet' | 'testnet' | 'regtest'
|
|
25
|
+
* @param internalKey - Configured FROST/Ika custody key (x-only, 32 bytes; required)
|
|
26
|
+
* @returns Taproot address (bc1p... or tb1p...)
|
|
27
|
+
*/
|
|
28
|
+
export function deriveTaprootAddress(commitment, network = "testnet", internalKey) {
|
|
29
|
+
if (!internalKey) {
|
|
30
|
+
throw new Error("Internal key is required; pass the configured FROST/Ika x-only key");
|
|
31
|
+
}
|
|
32
|
+
const key = internalKey;
|
|
33
|
+
if (key.length !== 32) {
|
|
34
|
+
throw new Error("Internal key must be 32 bytes (x-only)");
|
|
35
|
+
}
|
|
36
|
+
// Refuse the secp256k1 generator x-coord (discrete log = 1): its key-path secret
|
|
37
|
+
// 1+tweak is publicly computable, so any observer could sweep the output.
|
|
38
|
+
if (bytesToHex(key) === UNSAFE_GENERATOR_INTERNAL_KEY_HEX) {
|
|
39
|
+
throw new Error("Refusing to derive Taproot with the generator internal key; pass a real FROST/Ika key");
|
|
40
|
+
}
|
|
41
|
+
// Compute tweak = H_TapTweak(internal_key || commitment)
|
|
42
|
+
const tweakInput = new Uint8Array(64);
|
|
43
|
+
tweakInput.set(key, 0);
|
|
44
|
+
tweakInput.set(commitment, 32);
|
|
45
|
+
const tweak = taggedHash("TapTweak", tweakInput);
|
|
46
|
+
// BIP-341: output_key = lift_x(internal_key) + tweak * G
|
|
47
|
+
const tweakScalar = bytesToBigInt(tweak);
|
|
48
|
+
const SECP256K1_ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
49
|
+
if (tweakScalar >= SECP256K1_ORDER) {
|
|
50
|
+
throw new Error("Tweak scalar exceeds curve order");
|
|
51
|
+
}
|
|
52
|
+
// lift_x: recover full point from x-only key (even y per BIP-340)
|
|
53
|
+
const keyHex = "02" + bytesToHex(key);
|
|
54
|
+
const internalPoint = secp256k1.Point.fromHex(keyHex);
|
|
55
|
+
// tweak * G
|
|
56
|
+
const tweakPoint = secp256k1.Point.BASE.multiply(tweakScalar);
|
|
57
|
+
// Q = P + t*G
|
|
58
|
+
const outputPoint = internalPoint.add(tweakPoint);
|
|
59
|
+
// x-only output key (BIP-340): drop prefix from compressed form
|
|
60
|
+
const outputKeyHex = outputPoint.toHex(true); // 33-byte compressed hex
|
|
61
|
+
const outputKey = hexToBytes(outputKeyHex.slice(2)); // drop "02"/"03" prefix
|
|
62
|
+
// Encode as bech32m address
|
|
63
|
+
const hrp = network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
|
|
64
|
+
const words = bech32.bech32m.toWords(outputKey);
|
|
65
|
+
// Witness version 1 for taproot
|
|
66
|
+
const address = bech32.bech32m.encode(hrp, [1, ...words]);
|
|
67
|
+
return {
|
|
68
|
+
address,
|
|
69
|
+
outputKey,
|
|
70
|
+
tweak,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Verify that a Taproot address is correctly derived from a commitment
|
|
75
|
+
*
|
|
76
|
+
* @param address - Taproot address to verify
|
|
77
|
+
* @param commitment - Expected commitment
|
|
78
|
+
* @param internalKey - Configured FROST/Ika custody key (x-only, 32 bytes; required)
|
|
79
|
+
* @returns true if address matches expected derivation
|
|
80
|
+
*/
|
|
81
|
+
export function verifyTaprootAddress(address, commitment, internalKey) {
|
|
82
|
+
try {
|
|
83
|
+
const decoded = bech32.bech32m.decode(address);
|
|
84
|
+
const witnessVersion = decoded.words[0];
|
|
85
|
+
if (witnessVersion !== 1) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const actualOutputKey = new Uint8Array(bech32.bech32m.fromWords(decoded.words.slice(1)));
|
|
89
|
+
const network = decoded.prefix === "bc" ? "mainnet" : "testnet";
|
|
90
|
+
const expected = deriveTaprootAddress(commitment, network, internalKey);
|
|
91
|
+
return arraysEqual(actualOutputKey, expected.outputKey);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Generate a P2TR (Pay-to-Taproot) script pubkey
|
|
99
|
+
*
|
|
100
|
+
* @param outputKey - 32-byte output key (x-only)
|
|
101
|
+
* @returns Script pubkey bytes (OP_1 <32-byte key>)
|
|
102
|
+
*/
|
|
103
|
+
export function createP2TRScriptPubkey(outputKey) {
|
|
104
|
+
if (outputKey.length !== 32) {
|
|
105
|
+
throw new Error("Output key must be 32 bytes");
|
|
106
|
+
}
|
|
107
|
+
// OP_1 (0x51) + push 32 bytes (0x20) + key
|
|
108
|
+
const script = new Uint8Array(34);
|
|
109
|
+
script[0] = 0x51; // OP_1 (witness version 1)
|
|
110
|
+
script[1] = 0x20; // Push 32 bytes
|
|
111
|
+
script.set(outputKey, 2);
|
|
112
|
+
return script;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Parse P2TR script pubkey to extract output key
|
|
116
|
+
*
|
|
117
|
+
* @param scriptPubkey - Script pubkey bytes
|
|
118
|
+
* @returns Output key or null if not P2TR
|
|
119
|
+
*/
|
|
120
|
+
export function parseP2TRScriptPubkey(scriptPubkey) {
|
|
121
|
+
if (scriptPubkey.length !== 34)
|
|
122
|
+
return null;
|
|
123
|
+
if (scriptPubkey[0] !== 0x51)
|
|
124
|
+
return null; // OP_1
|
|
125
|
+
if (scriptPubkey[1] !== 0x20)
|
|
126
|
+
return null; // Push 32
|
|
127
|
+
return scriptPubkey.slice(2);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Validate a Bitcoin address format
|
|
131
|
+
*/
|
|
132
|
+
export function isValidBitcoinAddress(address) {
|
|
133
|
+
try {
|
|
134
|
+
// Bech32m (Taproot)
|
|
135
|
+
if (address.startsWith("bc1p") || address.startsWith("tb1p")) {
|
|
136
|
+
const decoded = bech32.bech32m.decode(address);
|
|
137
|
+
if (decoded.words[0] === 1 && decoded.words.length === 53) {
|
|
138
|
+
return {
|
|
139
|
+
valid: true,
|
|
140
|
+
type: "p2tr",
|
|
141
|
+
network: decoded.prefix === "bc" ? "mainnet" : "testnet",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Bech32 (SegWit v0)
|
|
146
|
+
if (address.startsWith("bc1q") ||
|
|
147
|
+
address.startsWith("tb1q") ||
|
|
148
|
+
address.startsWith("bcrt1q")) {
|
|
149
|
+
const decoded = bech32.bech32.decode(address);
|
|
150
|
+
if (decoded.words[0] === 0) {
|
|
151
|
+
const type = decoded.words.length === 33 ? "p2wpkh" : "p2wsh";
|
|
152
|
+
return {
|
|
153
|
+
valid: true,
|
|
154
|
+
type,
|
|
155
|
+
network: decoded.prefix === "bc"
|
|
156
|
+
? "mainnet"
|
|
157
|
+
: decoded.prefix === "bcrt"
|
|
158
|
+
? "testnet"
|
|
159
|
+
: "testnet",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Legacy (base58check)
|
|
164
|
+
const len = address.length;
|
|
165
|
+
if (len >= 26 && len <= 35) {
|
|
166
|
+
if (address.startsWith("1")) {
|
|
167
|
+
return { valid: true, type: "p2pkh", network: "mainnet" };
|
|
168
|
+
}
|
|
169
|
+
if (address.startsWith("3")) {
|
|
170
|
+
return { valid: true, type: "p2sh", network: "mainnet" };
|
|
171
|
+
}
|
|
172
|
+
if (address.startsWith("m") || address.startsWith("n")) {
|
|
173
|
+
return { valid: true, type: "p2pkh", network: "testnet" };
|
|
174
|
+
}
|
|
175
|
+
if (address.startsWith("2")) {
|
|
176
|
+
return { valid: true, type: "p2sh", network: "testnet" };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { valid: false, type: "unknown", network: "unknown" };
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return { valid: false, type: "unknown", network: "unknown" };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function bytesToBigInt(bytes) {
|
|
186
|
+
let result = 0n;
|
|
187
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
188
|
+
result = (result << 8n) | BigInt(bytes[i]);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function arraysEqual(a, b) {
|
|
193
|
+
if (a.length !== b.length)
|
|
194
|
+
return false;
|
|
195
|
+
for (let i = 0; i < a.length; i++) {
|
|
196
|
+
if (a[i] !== b[i])
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
// ========== OP_RETURN Helpers ==========
|
|
202
|
+
/** Destination chain encoded in the compact deposit OP_RETURN header. */
|
|
203
|
+
export const DEPOSIT_DESTINATION_CHAIN = {
|
|
204
|
+
SOLANA: 1,
|
|
205
|
+
};
|
|
206
|
+
/** Bitcoin network encoded in the compact deposit OP_RETURN header. */
|
|
207
|
+
export const DEPOSIT_BITCOIN_NETWORK = {
|
|
208
|
+
MAINNET: 0,
|
|
209
|
+
TESTNET4: 2,
|
|
210
|
+
REGTEST: 3,
|
|
211
|
+
};
|
|
212
|
+
export const DEPOSIT_OP_RETURN_VERSION = 1;
|
|
213
|
+
export const DEPOSIT_POOL_TAG_SIZE = 8;
|
|
214
|
+
/** OP_RETURN payload size for deposit: header(1) + poolTag(8) + ephemeralPubkey(32) + notePublicKey(32). */
|
|
215
|
+
export const DEPOSIT_OP_RETURN_SIZE = 73;
|
|
216
|
+
/**
|
|
217
|
+
* Build the compact deposit OP_RETURN payload for non-interactive stealth deposits.
|
|
218
|
+
*
|
|
219
|
+
* Layout:
|
|
220
|
+
* [0] header — version + destination chain + Bitcoin network
|
|
221
|
+
* [1..9) poolTag — destination deployment tag
|
|
222
|
+
* [9..41) ephemeralPubkey — Ed25519 public key
|
|
223
|
+
* [41..73) notePublicKey — Note public key (Poseidon hash)
|
|
224
|
+
*
|
|
225
|
+
* Amount is no longer embedded — the on-chain program reads it from the BTC output.
|
|
226
|
+
* The caller wraps this in an OP_RETURN script (0x6a + push opcode + payload).
|
|
227
|
+
*/
|
|
228
|
+
export function buildDepositOpReturn(ephemeralPubkey, notePublicKey, context) {
|
|
229
|
+
if (ephemeralPubkey.length !== 32)
|
|
230
|
+
throw new Error("ephemeralPubkey must be 32 bytes");
|
|
231
|
+
if (notePublicKey.length !== 32)
|
|
232
|
+
throw new Error("notePublicKey must be 32 bytes");
|
|
233
|
+
validateDepositOpReturnContext(context);
|
|
234
|
+
const payload = new Uint8Array(DEPOSIT_OP_RETURN_SIZE);
|
|
235
|
+
payload[0] = encodeDepositOpReturnHeader(context.destinationChain, context.bitcoinNetwork);
|
|
236
|
+
payload.set(context.poolTag, 1);
|
|
237
|
+
payload.set(ephemeralPubkey, 1 + DEPOSIT_POOL_TAG_SIZE);
|
|
238
|
+
payload.set(notePublicKey, 1 + DEPOSIT_POOL_TAG_SIZE + 32);
|
|
239
|
+
return payload;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Parse the compact deposit OP_RETURN payload back into its constituent fields.
|
|
243
|
+
*
|
|
244
|
+
* @returns Parsed fields, or null if data is not exactly the expected size.
|
|
245
|
+
*/
|
|
246
|
+
export function parseDepositOpReturn(data) {
|
|
247
|
+
if (data.length !== DEPOSIT_OP_RETURN_SIZE)
|
|
248
|
+
return null;
|
|
249
|
+
const header = decodeDepositOpReturnHeader(data[0]);
|
|
250
|
+
if (!header)
|
|
251
|
+
return null;
|
|
252
|
+
return {
|
|
253
|
+
...header,
|
|
254
|
+
poolTag: data.slice(1, 1 + DEPOSIT_POOL_TAG_SIZE),
|
|
255
|
+
ephemeralPubkey: data.slice(1 + DEPOSIT_POOL_TAG_SIZE, 1 + DEPOSIT_POOL_TAG_SIZE + 32),
|
|
256
|
+
notePublicKey: data.slice(1 + DEPOSIT_POOL_TAG_SIZE + 32, DEPOSIT_OP_RETURN_SIZE),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
export function encodeDepositOpReturnHeader(destinationChain, bitcoinNetwork) {
|
|
260
|
+
if (destinationChain !== DEPOSIT_DESTINATION_CHAIN.SOLANA) {
|
|
261
|
+
throw new Error("invalid deposit destination chain");
|
|
262
|
+
}
|
|
263
|
+
if (bitcoinNetwork !== DEPOSIT_BITCOIN_NETWORK.MAINNET
|
|
264
|
+
&& bitcoinNetwork !== DEPOSIT_BITCOIN_NETWORK.TESTNET4
|
|
265
|
+
&& bitcoinNetwork !== DEPOSIT_BITCOIN_NETWORK.REGTEST) {
|
|
266
|
+
throw new Error("invalid deposit bitcoin network");
|
|
267
|
+
}
|
|
268
|
+
return (DEPOSIT_OP_RETURN_VERSION << 6) | (destinationChain << 4) | bitcoinNetwork;
|
|
269
|
+
}
|
|
270
|
+
export function decodeDepositOpReturnHeader(header) {
|
|
271
|
+
const version = header >> 6;
|
|
272
|
+
const destinationChain = (header >> 4) & 0x03;
|
|
273
|
+
const bitcoinNetwork = header & 0x0f;
|
|
274
|
+
if (version !== DEPOSIT_OP_RETURN_VERSION)
|
|
275
|
+
return null;
|
|
276
|
+
if (destinationChain !== DEPOSIT_DESTINATION_CHAIN.SOLANA) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
if (bitcoinNetwork !== DEPOSIT_BITCOIN_NETWORK.MAINNET
|
|
280
|
+
&& bitcoinNetwork !== DEPOSIT_BITCOIN_NETWORK.TESTNET4
|
|
281
|
+
&& bitcoinNetwork !== DEPOSIT_BITCOIN_NETWORK.REGTEST) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
version,
|
|
286
|
+
destinationChain: destinationChain,
|
|
287
|
+
bitcoinNetwork: bitcoinNetwork,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
export function validateDepositOpReturnContext(context) {
|
|
291
|
+
encodeDepositOpReturnHeader(context.destinationChain, context.bitcoinNetwork);
|
|
292
|
+
if (context.poolTag.length !== DEPOSIT_POOL_TAG_SIZE) {
|
|
293
|
+
throw new Error(`poolTag must be ${DEPOSIT_POOL_TAG_SIZE} bytes`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
export function computeDepositPoolTag(parts) {
|
|
297
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
298
|
+
const bytes = new Uint8Array(total);
|
|
299
|
+
let offset = 0;
|
|
300
|
+
for (const part of parts) {
|
|
301
|
+
bytes.set(part, offset);
|
|
302
|
+
offset += part.length;
|
|
303
|
+
}
|
|
304
|
+
return sha256(bytes).slice(0, DEPOSIT_POOL_TAG_SIZE);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Create an OP_RETURN script from an arbitrary payload (up to 80 bytes).
|
|
308
|
+
*
|
|
309
|
+
* Format: OP_RETURN (0x6a) + OP_PUSHDATA (length byte) + payload
|
|
310
|
+
*/
|
|
311
|
+
export function createOpReturnScriptFromPayload(payload) {
|
|
312
|
+
if (payload.length > 80)
|
|
313
|
+
throw new Error("OP_RETURN payload exceeds 80 bytes");
|
|
314
|
+
// For payloads <= 75 bytes, use a single-byte push opcode (OP_PUSH_N).
|
|
315
|
+
// For 76..80 bytes, use OP_PUSHDATA1 (0x4c) + 1-byte length.
|
|
316
|
+
if (payload.length <= 75) {
|
|
317
|
+
const script = new Uint8Array(2 + payload.length);
|
|
318
|
+
script[0] = 0x6a; // OP_RETURN
|
|
319
|
+
script[1] = payload.length; // direct push opcode
|
|
320
|
+
script.set(payload, 2);
|
|
321
|
+
return script;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const script = new Uint8Array(3 + payload.length);
|
|
325
|
+
script[0] = 0x6a; // OP_RETURN
|
|
326
|
+
script[1] = 0x4c; // OP_PUSHDATA1
|
|
327
|
+
script[2] = payload.length;
|
|
328
|
+
script.set(payload, 3);
|
|
329
|
+
return script;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// ========== Refund Script Taproot Helpers ==========
|
|
333
|
+
/**
|
|
334
|
+
* Encode an integer as Bitcoin Script compact size (CompactSize/varint).
|
|
335
|
+
* - 0-252: single byte
|
|
336
|
+
* - 253-65535: 0xfd + 2 bytes LE
|
|
337
|
+
*/
|
|
338
|
+
function compactSizeEncode(n) {
|
|
339
|
+
if (n < 0)
|
|
340
|
+
throw new Error("compactSize cannot be negative");
|
|
341
|
+
if (n <= 252) {
|
|
342
|
+
return new Uint8Array([n]);
|
|
343
|
+
}
|
|
344
|
+
if (n <= 0xffff) {
|
|
345
|
+
const buf = new Uint8Array(3);
|
|
346
|
+
buf[0] = 0xfd;
|
|
347
|
+
buf[1] = n & 0xff;
|
|
348
|
+
buf[2] = (n >> 8) & 0xff;
|
|
349
|
+
return buf;
|
|
350
|
+
}
|
|
351
|
+
throw new Error("compactSize > 65535 not supported");
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Build a refund script for time-locked user recovery.
|
|
355
|
+
*
|
|
356
|
+
* Script:
|
|
357
|
+
* <npk_32> OP_DROP <144> OP_CHECKSEQUENCEVERIFY OP_DROP <user_x_only_pubkey_32> OP_CHECKSIG
|
|
358
|
+
*
|
|
359
|
+
* @param npk - 32-byte note public key (commitment binding)
|
|
360
|
+
* @param userPubkey - 32-byte x-only public key for the refund path
|
|
361
|
+
* @returns Script bytes (73 bytes)
|
|
362
|
+
*/
|
|
363
|
+
export function buildRefundScript(npk, userPubkey) {
|
|
364
|
+
if (npk.length !== 32)
|
|
365
|
+
throw new Error("npk must be 32 bytes");
|
|
366
|
+
if (userPubkey.length !== 32)
|
|
367
|
+
throw new Error("userPubkey must be 32 bytes (x-only)");
|
|
368
|
+
// Total: 1+32+1+1+2+1+1+1+32+1 = 73 bytes
|
|
369
|
+
const script = new Uint8Array(73);
|
|
370
|
+
let offset = 0;
|
|
371
|
+
// OP_PUSHBYTES_32 + npk
|
|
372
|
+
script[offset++] = 0x20;
|
|
373
|
+
script.set(npk, offset);
|
|
374
|
+
offset += 32;
|
|
375
|
+
// OP_DROP
|
|
376
|
+
script[offset++] = 0x75;
|
|
377
|
+
// Push 144 as minimal signed LE: 144 = 0x90, high bit set → needs 0x00 padding → [0x90, 0x00]
|
|
378
|
+
// OP_PUSHBYTES_2
|
|
379
|
+
script[offset++] = 0x02;
|
|
380
|
+
script[offset++] = 0x90;
|
|
381
|
+
script[offset++] = 0x00;
|
|
382
|
+
// OP_CHECKSEQUENCEVERIFY
|
|
383
|
+
script[offset++] = 0xb2;
|
|
384
|
+
// OP_DROP
|
|
385
|
+
script[offset++] = 0x75;
|
|
386
|
+
// OP_PUSHBYTES_32 + user x-only pubkey
|
|
387
|
+
script[offset++] = 0x20;
|
|
388
|
+
script.set(userPubkey, offset);
|
|
389
|
+
offset += 32;
|
|
390
|
+
// OP_CHECKSIG
|
|
391
|
+
script[offset++] = 0xac;
|
|
392
|
+
return script;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Compute a TapLeaf hash per BIP-341.
|
|
396
|
+
*
|
|
397
|
+
* TapLeaf = H_TapLeaf(leafVersion || compactSize(script.length) || script)
|
|
398
|
+
*
|
|
399
|
+
* @param script - The leaf script bytes
|
|
400
|
+
* @param leafVersion - Leaf version byte (default 0xc0)
|
|
401
|
+
* @returns 32-byte tagged hash
|
|
402
|
+
*/
|
|
403
|
+
export function computeTapLeafHash(script, leafVersion = 0xc0) {
|
|
404
|
+
const scriptLenBytes = compactSizeEncode(script.length);
|
|
405
|
+
const data = new Uint8Array(1 + scriptLenBytes.length + script.length);
|
|
406
|
+
data[0] = leafVersion;
|
|
407
|
+
data.set(scriptLenBytes, 1);
|
|
408
|
+
data.set(script, 1 + scriptLenBytes.length);
|
|
409
|
+
return taggedHash("TapLeaf", data);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Derive a Taproot address with a refund script path.
|
|
413
|
+
*
|
|
414
|
+
* The address commits to both the FROST group key (internal key) and a
|
|
415
|
+
* time-locked refund script that allows the user to reclaim funds after
|
|
416
|
+
* 144 blocks (~1 day) if the bridge fails to sweep.
|
|
417
|
+
*
|
|
418
|
+
* Taproot construction:
|
|
419
|
+
* - Internal key = FROST group key (x-only)
|
|
420
|
+
* - Single TapLeaf = refund script
|
|
421
|
+
* - Merkle root = TapLeaf hash (single leaf, no branching)
|
|
422
|
+
* - Tweak = H_TapTweak(internal_key || merkle_root)
|
|
423
|
+
* - Output key = internal_key + tweak * G
|
|
424
|
+
*
|
|
425
|
+
* @param npk - 32-byte note public key (embedded in refund script for binding)
|
|
426
|
+
* @param userRefundPubkey - 32-byte x-only pubkey for the refund spending path
|
|
427
|
+
* @param internalKey - 32-byte x-only FROST group public key
|
|
428
|
+
* @param network - Bitcoin network for address encoding
|
|
429
|
+
*/
|
|
430
|
+
export function deriveTaprootAddressWithRefund(npk, userRefundPubkey, internalKey, network = "testnet") {
|
|
431
|
+
if (internalKey.length !== 32)
|
|
432
|
+
throw new Error("Internal key must be 32 bytes (x-only)");
|
|
433
|
+
if (npk.length !== 32)
|
|
434
|
+
throw new Error("npk must be 32 bytes");
|
|
435
|
+
if (userRefundPubkey.length !== 32)
|
|
436
|
+
throw new Error("userRefundPubkey must be 32 bytes (x-only)");
|
|
437
|
+
// 1. Build the refund script
|
|
438
|
+
const refundScript = buildRefundScript(npk, userRefundPubkey);
|
|
439
|
+
// 2. Compute the TapLeaf hash (single leaf = merkle root)
|
|
440
|
+
const merkleRoot = computeTapLeafHash(refundScript);
|
|
441
|
+
// 3. Compute tweak = H_TapTweak(internal_key || merkle_root)
|
|
442
|
+
const tweakInput = new Uint8Array(64);
|
|
443
|
+
tweakInput.set(internalKey, 0);
|
|
444
|
+
tweakInput.set(merkleRoot, 32);
|
|
445
|
+
const tweak = taggedHash("TapTweak", tweakInput);
|
|
446
|
+
// 4. Compute output key = lift_x(internal_key) + tweak * G
|
|
447
|
+
const tweakScalar = bytesToBigInt(tweak);
|
|
448
|
+
const SECP256K1_ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
449
|
+
if (tweakScalar >= SECP256K1_ORDER) {
|
|
450
|
+
throw new Error("Tweak scalar exceeds curve order");
|
|
451
|
+
}
|
|
452
|
+
const keyHex = "02" + bytesToHex(internalKey);
|
|
453
|
+
const internalPoint = secp256k1.Point.fromHex(keyHex);
|
|
454
|
+
const tweakPoint = secp256k1.Point.BASE.multiply(tweakScalar);
|
|
455
|
+
const outputPoint = internalPoint.add(tweakPoint);
|
|
456
|
+
const outputKeyHex = outputPoint.toHex(true); // 33-byte compressed hex
|
|
457
|
+
const outputKey = hexToBytes(outputKeyHex.slice(2)); // drop "02"/"03" prefix
|
|
458
|
+
// 5. Determine parity bit for the control block
|
|
459
|
+
const parityBit = outputKeyHex.startsWith("03") ? 1 : 0;
|
|
460
|
+
// 6. Build the control block: <leaf_version | parity_bit> <internal_key>
|
|
461
|
+
const controlBlock = new Uint8Array(33);
|
|
462
|
+
controlBlock[0] = 0xc0 | parityBit;
|
|
463
|
+
controlBlock.set(internalKey, 1);
|
|
464
|
+
// 7. Encode as bech32m address
|
|
465
|
+
const hrp = network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
|
|
466
|
+
const words = bech32.bech32m.toWords(outputKey);
|
|
467
|
+
const address = bech32.bech32m.encode(hrp, [1, ...words]);
|
|
468
|
+
return {
|
|
469
|
+
address,
|
|
470
|
+
outputKey,
|
|
471
|
+
merkleRoot,
|
|
472
|
+
controlBlock,
|
|
473
|
+
refundScript,
|
|
474
|
+
tweak,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Get the internal key used by UTXOpia
|
|
479
|
+
* In production, this would be the FROST threshold public key
|
|
480
|
+
*/
|
|
481
|
+
export function getInternalKey() {
|
|
482
|
+
// The historical default is the secp256k1 generator (sweepable). Refuse it —
|
|
483
|
+
// callers must supply a real FROST/Ika group key explicitly.
|
|
484
|
+
throw new Error("getInternalKey() is disabled: configure a real FROST/Ika internal key");
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Set a custom internal key (for testing or custom deployments)
|
|
488
|
+
*/
|
|
489
|
+
export function createCustomInternalKey(key) {
|
|
490
|
+
if (key.length !== 32) {
|
|
491
|
+
throw new Error("Internal key must be 32 bytes (x-only pubkey)");
|
|
492
|
+
}
|
|
493
|
+
return new Uint8Array(key);
|
|
494
|
+
}
|