|
| 1 | +import init, { |
| 2 | + RlnWasmInvoice, |
| 3 | + RlnWasmSdk, |
| 4 | + rgbGenerateKeysValue, |
| 5 | +} from "../../pkg/rln_wasm_sdk.js"; |
| 6 | + |
| 7 | +const DEFAULT_INDEXER_URL = "http://127.0.0.1:3002"; |
| 8 | +const DEFAULT_TRANSPORT_ENDPOINT = "rpc://127.0.0.1:3000/json-rpc"; |
| 9 | + |
| 10 | +function log(message, data = undefined) { |
| 11 | + const out = document.getElementById("out"); |
| 12 | + if (!out) return; |
| 13 | + const line = document.createElement("pre"); |
| 14 | + line.textContent = |
| 15 | + data === undefined |
| 16 | + ? String(message) |
| 17 | + : `${message}: ${JSON.stringify(data, null, 2)}`; |
| 18 | + out.appendChild(line); |
| 19 | +} |
| 20 | + |
| 21 | +function readText(id) { |
| 22 | + const el = document.getElementById(id); |
| 23 | + return el && typeof el.value === "string" ? el.value.trim() : ""; |
| 24 | +} |
| 25 | + |
| 26 | +function readPositiveInt(id, fallback) { |
| 27 | + const raw = readText(id); |
| 28 | + if (!raw) return fallback; |
| 29 | + const n = Number.parseInt(raw, 10); |
| 30 | + if (!Number.isFinite(n) || n <= 0) { |
| 31 | + throw new Error(`${id} must be a positive integer`); |
| 32 | + } |
| 33 | + return n; |
| 34 | +} |
| 35 | + |
| 36 | +function parseWalletData(id) { |
| 37 | + const raw = readText(id); |
| 38 | + if (!raw) { |
| 39 | + throw new Error(`${id} cannot be empty`); |
| 40 | + } |
| 41 | + let parsed; |
| 42 | + try { |
| 43 | + parsed = JSON.parse(raw); |
| 44 | + } catch (err) { |
| 45 | + throw new Error(`${id} is not valid JSON: ${String(err)}`); |
| 46 | + } |
| 47 | + return JSON.stringify(parsed); |
| 48 | +} |
| 49 | + |
| 50 | +function buildWalletDataFromGeneratedKeys(keys, role, seed) { |
| 51 | + return { |
| 52 | + data_dir: `/tmp/rln_wasm_${role}_${seed}`, |
| 53 | + bitcoin_network: "Regtest", |
| 54 | + database_type: "Sqlite", |
| 55 | + max_allocations_per_utxo: 5, |
| 56 | + account_xpub_vanilla: keys.account_xpub_vanilla, |
| 57 | + account_xpub_colored: keys.account_xpub_colored, |
| 58 | + mnemonic: keys.mnemonic, |
| 59 | + master_fingerprint: keys.master_fingerprint, |
| 60 | + vanilla_keychain: null, |
| 61 | + supported_schemas: ["Nia"], |
| 62 | + }; |
| 63 | +} |
| 64 | + |
| 65 | +function ensureWalletDataInputsFilled() { |
| 66 | + const senderWalletInput = document.getElementById("senderWalletData"); |
| 67 | + const receiverWalletInput = document.getElementById("receiverWalletData"); |
| 68 | + if (!senderWalletInput || !receiverWalletInput) { |
| 69 | + return; |
| 70 | + } |
| 71 | + if (senderWalletInput.value.trim() && receiverWalletInput.value.trim()) { |
| 72 | + return; |
| 73 | + } |
| 74 | + |
| 75 | + const seed = Date.now().toString(); |
| 76 | + const senderKeys = rgbGenerateKeysValue("regtest"); |
| 77 | + const receiverKeys = rgbGenerateKeysValue("regtest"); |
| 78 | + const senderWalletData = buildWalletDataFromGeneratedKeys(senderKeys, "sender", seed); |
| 79 | + const receiverWalletData = buildWalletDataFromGeneratedKeys(receiverKeys, "receiver", seed); |
| 80 | + |
| 81 | + senderWalletInput.value = JSON.stringify(senderWalletData, null, 2); |
| 82 | + receiverWalletInput.value = JSON.stringify(receiverWalletData, null, 2); |
| 83 | + log("WalletData generated at runtime", { |
| 84 | + sender_mnemonic_words: senderKeys.mnemonic.split(" ").length, |
| 85 | + receiver_mnemonic_words: receiverKeys.mnemonic.split(" ").length, |
| 86 | + }); |
| 87 | +} |
| 88 | + |
| 89 | +async function signPsbt(unsignedPsbt) { |
| 90 | + if (typeof window.signPsbt === "function") { |
| 91 | + const signed = await window.signPsbt(unsignedPsbt); |
| 92 | + if (!signed || typeof signed !== "string") { |
| 93 | + throw new Error("window.signPsbt returned an invalid signed PSBT"); |
| 94 | + } |
| 95 | + return signed; |
| 96 | + } |
| 97 | + |
| 98 | + throw new Error( |
| 99 | + "No signer configured. Inject window.signPsbt(unsignedPsbt) => signedPsbt before running the flow." |
| 100 | + ); |
| 101 | +} |
| 102 | + |
| 103 | +async function ensureRgbAllocations(walletHandle, online) { |
| 104 | + const before = walletHandle.getBtcBalanceValue(); |
| 105 | + const coloredSpendable = |
| 106 | + before && before.colored && typeof before.colored.spendable === "number" |
| 107 | + ? before.colored.spendable |
| 108 | + : 0; |
| 109 | + if (coloredSpendable > 0) { |
| 110 | + return before; |
| 111 | + } |
| 112 | + |
| 113 | + log("No colored allocations, creating UTXOs", { before }); |
| 114 | + const unsignedPsbt = await walletHandle.createUtxosBegin( |
| 115 | + online, |
| 116 | + true, |
| 117 | + 5, |
| 118 | + undefined, |
| 119 | + 1n, |
| 120 | + false |
| 121 | + ); |
| 122 | + log("createUtxosBegin ready", { unsigned_psbt_length: unsignedPsbt.length }); |
| 123 | + |
| 124 | + const signedPsbt = await signPsbt(unsignedPsbt); |
| 125 | + const created = await walletHandle.createUtxosEnd(online, signedPsbt, false); |
| 126 | + log("createUtxosEnd result", { created }); |
| 127 | + |
| 128 | + await walletHandle.syncOnline(online); |
| 129 | + const after = walletHandle.getBtcBalanceValue(); |
| 130 | + log("BTC balances after createUtxos", after); |
| 131 | + return after; |
| 132 | +} |
| 133 | + |
| 134 | +function pickInvoiceString(invoiceResponse) { |
| 135 | + if (typeof invoiceResponse === "string") return invoiceResponse; |
| 136 | + if (invoiceResponse && typeof invoiceResponse.invoice === "string") { |
| 137 | + return invoiceResponse.invoice; |
| 138 | + } |
| 139 | + if (invoiceResponse && typeof invoiceResponse.invoice_string === "string") { |
| 140 | + return invoiceResponse.invoice_string; |
| 141 | + } |
| 142 | + throw new Error("blindReceiveValue response does not contain invoice string"); |
| 143 | +} |
| 144 | + |
| 145 | +function pickTransportEndpoints(invoiceData, fallbackEndpoint) { |
| 146 | + const endpoints = |
| 147 | + invoiceData.transport_endpoints || invoiceData.transportEndpoints || []; |
| 148 | + if (Array.isArray(endpoints) && endpoints.length > 0) { |
| 149 | + return endpoints; |
| 150 | + } |
| 151 | + if (fallbackEndpoint) { |
| 152 | + return [fallbackEndpoint]; |
| 153 | + } |
| 154 | + throw new Error("No transport endpoints available for recipient"); |
| 155 | +} |
| 156 | + |
| 157 | +async function run() { |
| 158 | + const out = document.getElementById("out"); |
| 159 | + if (out) out.innerHTML = ""; |
| 160 | + |
| 161 | + await init(); |
| 162 | + log("WASM init", { ok: true }); |
| 163 | + ensureWalletDataInputsFilled(); |
| 164 | + |
| 165 | + const indexerUrl = readText("indexerUrl"); |
| 166 | + const transportEndpoint = readText("transportEndpoint"); |
| 167 | + const issueAmount = readPositiveInt("issueAmount", 1000); |
| 168 | + const sendAmount = readPositiveInt("sendAmount", 100); |
| 169 | + |
| 170 | + if (!indexerUrl) { |
| 171 | + throw new Error("indexerUrl cannot be empty"); |
| 172 | + } |
| 173 | + if (!transportEndpoint) { |
| 174 | + throw new Error("transportEndpoint cannot be empty"); |
| 175 | + } |
| 176 | + if (sendAmount > issueAmount) { |
| 177 | + throw new Error("sendAmount cannot be greater than issueAmount"); |
| 178 | + } |
| 179 | + |
| 180 | + const senderWalletDataJson = parseWalletData("senderWalletData"); |
| 181 | + const receiverWalletDataJson = parseWalletData("receiverWalletData"); |
| 182 | + |
| 183 | + const sdk = new RlnWasmSdk(); |
| 184 | + const sender = await sdk.createWalletHandleAsync(senderWalletDataJson); |
| 185 | + const receiver = await sdk.createWalletHandleAsync(receiverWalletDataJson); |
| 186 | + log("Wallet handles created", { ok: true }); |
| 187 | + log("Wallet addresses", { |
| 188 | + sender_address: sender.getAddress(), |
| 189 | + receiver_address: receiver.getAddress(), |
| 190 | + }); |
| 191 | + |
| 192 | + const senderOnline = await sender.goOnlineValue(false, indexerUrl); |
| 193 | + const receiverOnline = await receiver.goOnlineValue(false, indexerUrl); |
| 194 | + log("Wallets online", { |
| 195 | + sender_online_id: senderOnline.id, |
| 196 | + receiver_online_id: receiverOnline.id, |
| 197 | + }); |
| 198 | + |
| 199 | + await sender.syncOnline(senderOnline); |
| 200 | + await receiver.syncOnline(receiverOnline); |
| 201 | + log("Initial sync complete", { ok: true }); |
| 202 | + log("BTC balances before issue", { |
| 203 | + sender: sender.getBtcBalanceValue(), |
| 204 | + receiver: receiver.getBtcBalanceValue(), |
| 205 | + }); |
| 206 | + await ensureRgbAllocations(sender, senderOnline); |
| 207 | + |
| 208 | + const issueReq = { |
| 209 | + ticker: "TST", |
| 210 | + name: "WASM RGB Demo", |
| 211 | + precision: 0, |
| 212 | + amounts: [issueAmount], |
| 213 | + }; |
| 214 | + const issued = sender.issueAssetNiaValue(issueReq); |
| 215 | + const assetId = issued.asset_id; |
| 216 | + log("Asset issued", issued); |
| 217 | + |
| 218 | + const receiveData = receiver.blindReceiveValue( |
| 219 | + assetId, |
| 220 | + { Fungible: sendAmount }, |
| 221 | + 3600, |
| 222 | + [transportEndpoint], |
| 223 | + 1 |
| 224 | + ); |
| 225 | + log("Receiver blind invoice", receiveData); |
| 226 | + |
| 227 | + const invoiceString = pickInvoiceString(receiveData); |
| 228 | + const invoiceObj = new RlnWasmInvoice(invoiceString); |
| 229 | + const invoiceData = invoiceObj.invoiceDataValue(); |
| 230 | + log("Decoded RGB invoice", invoiceData); |
| 231 | + |
| 232 | + const recipient = { |
| 233 | + recipient_id: invoiceData.recipient_id || invoiceData.recipientId, |
| 234 | + witness_data: null, |
| 235 | + assignment: { Fungible: sendAmount }, |
| 236 | + transport_endpoints: pickTransportEndpoints(invoiceData, transportEndpoint), |
| 237 | + }; |
| 238 | + if (!recipient.recipient_id) { |
| 239 | + throw new Error("Recipient id missing in decoded RGB invoice"); |
| 240 | + } |
| 241 | + |
| 242 | + const recipientMap = { |
| 243 | + [assetId]: [recipient], |
| 244 | + }; |
| 245 | + |
| 246 | + const unsignedPsbt = await sender.sendBegin(senderOnline, recipientMap, false, 1n, 1); |
| 247 | + log("Unsigned PSBT ready", { unsigned_psbt_length: unsignedPsbt.length }); |
| 248 | + |
| 249 | + const signedPsbt = await signPsbt(unsignedPsbt); |
| 250 | + log("Signed PSBT obtained", { signed_psbt_length: signedPsbt.length }); |
| 251 | + |
| 252 | + const sendResult = await sender.sendEndValue(senderOnline, signedPsbt, false); |
| 253 | + log("sendEnd result", sendResult); |
| 254 | + |
| 255 | + await sender.syncOnline(senderOnline); |
| 256 | + await receiver.syncOnline(receiverOnline); |
| 257 | + |
| 258 | + const senderBalance = sender.getAssetBalanceValue(assetId); |
| 259 | + const receiverBalance = receiver.getAssetBalanceValue(assetId); |
| 260 | + log("Sender asset balance", senderBalance); |
| 261 | + log("Receiver asset balance", receiverBalance); |
| 262 | + |
| 263 | + log("RGB transfer flow completed", { |
| 264 | + ok: true, |
| 265 | + asset_id: assetId, |
| 266 | + sent_amount: sendAmount, |
| 267 | + }); |
| 268 | +} |
| 269 | + |
| 270 | +const runBtn = document.getElementById("run"); |
| 271 | +if (runBtn) { |
| 272 | + runBtn.addEventListener("click", () => { |
| 273 | + run().catch((err) => { |
| 274 | + log("RGB transfer flow failed", String(err)); |
| 275 | + }); |
| 276 | + }); |
| 277 | +} |
| 278 | + |
| 279 | +const indexerInput = document.getElementById("indexerUrl"); |
| 280 | +if (indexerInput && !indexerInput.value.trim()) { |
| 281 | + indexerInput.value = DEFAULT_INDEXER_URL; |
| 282 | +} |
| 283 | + |
| 284 | +const transportInput = document.getElementById("transportEndpoint"); |
| 285 | +if (transportInput && !transportInput.value.trim()) { |
| 286 | + transportInput.value = DEFAULT_TRANSPORT_ENDPOINT; |
| 287 | +} |
| 288 | + |
| 289 | +const senderWalletInput = document.getElementById("senderWalletData"); |
| 290 | +const receiverWalletInput = document.getElementById("receiverWalletData"); |
| 291 | +if ( |
| 292 | + senderWalletInput && |
| 293 | + receiverWalletInput && |
| 294 | + !senderWalletInput.value.trim() && |
| 295 | + !receiverWalletInput.value.trim() |
| 296 | +) { |
| 297 | + // Generate once on page load for convenience. |
| 298 | + // A new runtime-generated pair will also be created in run() when fields are empty. |
| 299 | + const seed = Date.now().toString(); |
| 300 | + const senderKeys = rgbGenerateKeysValue("regtest"); |
| 301 | + const receiverKeys = rgbGenerateKeysValue("regtest"); |
| 302 | + senderWalletInput.value = JSON.stringify( |
| 303 | + buildWalletDataFromGeneratedKeys(senderKeys, "sender", seed), |
| 304 | + null, |
| 305 | + 2 |
| 306 | + ); |
| 307 | + receiverWalletInput.value = JSON.stringify( |
| 308 | + buildWalletDataFromGeneratedKeys(receiverKeys, "receiver", seed), |
| 309 | + null, |
| 310 | + 2 |
| 311 | + ); |
| 312 | +} |
0 commit comments