Skip to content

Commit 3eeaaef

Browse files
committed
Add nia asste wasm example
1 parent 1c12791 commit 3eeaaef

5 files changed

Lines changed: 465 additions & 4 deletions

File tree

bindings/wasm-sdk/examples/wasm-interop/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,30 @@ http://localhost:8080/bindings/wasm-sdk/examples/wasm-interop/virtual_channels_f
8282

8383
Click `Run Flow`.
8484

85+
For RGB asset issuance + transfer flow, open:
86+
87+
```text
88+
http://localhost:8080/bindings/wasm-sdk/examples/wasm-interop/rgb_asset_transfer_flow.html
89+
```
90+
91+
Click `Run RGB Transfer Flow`.
92+
8593
## Notes
8694

8795
1. This example is browser-only (`--target web`).
8896
2. It focuses on deterministic interop surface checks, not full native RLN node runtime behavior.
8997
3. Peer-session start may fail in normal local runs if proxy/peer is not reachable; the example logs this as non-fatal.
98+
4. `rgb_asset_transfer_flow.html` requires a PSBT signer callback in the page context:
99+
100+
```js
101+
window.signPsbt = async (unsignedPsbt) => {
102+
// return signed PSBT string
103+
return unsignedPsbt;
104+
};
105+
```
106+
107+
In real usage, replace this with your wallet/hardware signer integration.
108+
5. The RGB transfer page now pre-fills defaults for:
109+
- `Indexer URL`: `http://127.0.0.1:3002`
110+
- `RGB transport endpoint`: `rpc://127.0.0.1:3000/json-rpc`
111+
- sender/receiver `walletData` JSON generated at runtime via `rgbGenerateKeysValue("regtest")`
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
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

Comments
 (0)