signer.mx raw
1 package main
2
3 import (
4 "smesh.lol/web/common/helpers"
5 "smesh.lol/web/common/jsbridge/dom"
6 "smesh.lol/web/common/jsbridge/localstorage"
7 "smesh.lol/web/common/jsbridge/signer"
8 )
9
10 // Signer modal overlay inside the sm3sh app.
11 // Communicates with the extension background via window.nostr.smesh.
12
13 var (
14 signerModal dom.Element
15 signerContent dom.Element
16 signerOpen bool
17 signerStatus string // "none", "locked", "unlocked"
18 pendingK0Name string // nickname to publish as kind 0 after login
19 )
20
21 // initSigner checks for the extension and sets up the signer button.
22 func initSigner() {
23 signer.IsInstalled(func(ok bool) {
24 if !ok {
25 dom.ConsoleLog("signer extension not detected")
26 return
27 }
28 dom.ConsoleLog("signer extension detected")
29 refreshVaultStatus()
30 })
31 }
32
33 func refreshVaultStatus() {
34 signer.GetVaultStatus(func(status string) {
35 signerStatus = status
36 })
37 }
38
39 // showSignerModal creates and shows the signer modal overlay.
40 func showSignerModal() {
41 if signerOpen {
42 return
43 }
44 signerOpen = true
45
46 // Backdrop.
47 signerModal = dom.CreateElement("div")
48 dom.SetAttribute(signerModal, "class", "signer-backdrop")
49
50 // Modal card.
51 card := dom.CreateElement("div")
52 dom.SetAttribute(card, "class", "signer-card")
53
54 // Header.
55 header := dom.CreateElement("div")
56 dom.SetAttribute(header, "class", "signer-header")
57 title := dom.CreateElement("h2")
58 dom.SetTextContent(title, t("signer"))
59 dom.AppendChild(header, title)
60
61 closeBtn := dom.CreateElement("button")
62 dom.SetAttribute(closeBtn, "class", "signer-close")
63 dom.SetTextContent(closeBtn, "\u00d7")
64 closeCB := dom.RegisterCallback(func() { hideSignerModal() })
65 dom.AddEventListener(closeBtn, "click", closeCB)
66 dom.AppendChild(header, closeBtn)
67 dom.AppendChild(card, header)
68
69 // Content area.
70 signerContent = dom.CreateElement("div")
71 dom.SetAttribute(signerContent, "class", "signer-content")
72 dom.AppendChild(card, signerContent)
73
74 dom.AppendChild(signerModal, card)
75
76 // Click backdrop (not card children) to close.
77 backdropCB := dom.RegisterCallback(func() { hideSignerModal() })
78 dom.AddSelfEventListener(signerModal, "click", backdropCB)
79
80 dom.AppendChild(dom.Body(), signerModal)
81
82 // Render based on vault status.
83 renderSignerUI()
84 }
85
86 func hideSignerModal() {
87 if !signerOpen {
88 return
89 }
90 signerOpen = false
91 dom.RemoveChild(dom.Body(), signerModal)
92 }
93
94 func renderSignerUI() {
95 dom.SetTextContent(signerContent, "")
96
97 signer.GetVaultStatus(func(status string) {
98 signerStatus = status
99 dom.SetTextContent(signerContent, "")
100
101 switch status {
102 case "none":
103 renderCreateVault()
104 case "locked":
105 renderUnlockVault()
106 case "unlocked":
107 renderIdentityList()
108 }
109 })
110 }
111
112 func renderCreateVault() {
113 p := dom.CreateElement("p")
114 dom.SetTextContent(p, t("no_vault"))
115 dom.AppendChild(signerContent, p)
116
117 // --- HD Vault (default) ---
118 h3 := dom.CreateElement("h3")
119 dom.SetTextContent(h3, t("hd_keychain"))
120 dom.AppendChild(signerContent, h3)
121
122 pwInput := dom.CreateElement("input")
123 dom.SetAttribute(pwInput, "type", "password")
124 dom.SetAttribute(pwInput, "placeholder", t("vault_password"))
125 dom.SetAttribute(pwInput, "class", "signer-input")
126 dom.AppendChild(signerContent, pwInput)
127
128 createBtn := dom.CreateElement("button")
129 dom.SetAttribute(createBtn, "class", "signer-btn")
130 dom.SetTextContent(createBtn, t("generate_keychain"))
131 createCB := dom.RegisterCallback(func() {
132 pw := dom.GetProperty(pwInput, "value")
133 if pw == "" {
134 return
135 }
136 name := "Identity 0"
137 dom.SetTextContent(createBtn, t("generating"))
138 dom.SetAttribute(createBtn, "disabled", "true")
139 signer.CreateHDVault(pw, name, func(mnemonic string) {
140 if mnemonic == "" {
141 dom.SetTextContent(createBtn, t("create_failed"))
142 dom.SetAttribute(createBtn, "disabled", "")
143 return
144 }
145 // Derive Identity 1 so the user has a visible identity after seed reveal.
146 signer.DeriveIdentity("Identity 1", func(pk string) {
147 showMnemonicReveal(mnemonic)
148 })
149 })
150 })
151 dom.AddEventListener(createBtn, "click", createCB)
152 dom.AppendChild(signerContent, createBtn)
153
154 // --- Restore from mnemonic ---
155 sep := dom.CreateElement("hr")
156 dom.SetAttribute(sep, "class", "signer-sep")
157 dom.AppendChild(signerContent, sep)
158
159 h3r := dom.CreateElement("h3")
160 dom.SetTextContent(h3r, t("restore_seed"))
161 dom.AppendChild(signerContent, h3r)
162
163 mnInput := dom.CreateElement("textarea")
164 dom.SetAttribute(mnInput, "placeholder", t("seed_placeholder"))
165 dom.SetAttribute(mnInput, "class", "signer-input signer-textarea")
166 dom.SetAttribute(mnInput, "rows", "3")
167 dom.AppendChild(signerContent, mnInput)
168
169 rpwInput := dom.CreateElement("input")
170 dom.SetAttribute(rpwInput, "type", "password")
171 dom.SetAttribute(rpwInput, "placeholder", t("vault_password"))
172 dom.SetAttribute(rpwInput, "class", "signer-input")
173 dom.AppendChild(signerContent, rpwInput)
174
175 idxInput := dom.CreateElement("input")
176 dom.SetAttribute(idxInput, "type", "number")
177 dom.SetAttribute(idxInput, "placeholder", t("account_placeholder"))
178 dom.SetAttribute(idxInput, "class", "signer-input")
179 dom.SetProperty(idxInput, "value", "1")
180 dom.AppendChild(signerContent, idxInput)
181
182 restoreBtn := dom.CreateElement("button")
183 dom.SetAttribute(restoreBtn, "class", "signer-btn")
184 dom.SetTextContent(restoreBtn, t("restore_keychain"))
185 restoreCB := dom.RegisterCallback(func() {
186 mn := dom.GetProperty(mnInput, "value")
187 pw := dom.GetProperty(rpwInput, "value")
188 if mn == "" || pw == "" {
189 return
190 }
191 idxStr := dom.GetProperty(idxInput, "value")
192 targetIdx := parseAccountIdx(idxStr)
193 if targetIdx < 1 {
194 targetIdx = 1
195 }
196 dom.SetTextContent(restoreBtn, t("restoring"))
197 dom.SetAttribute(restoreBtn, "disabled", "true")
198 signer.RestoreHDVault(pw, mn, "Identity 0", func(ok bool) {
199 if !ok {
200 dom.SetTextContent(restoreBtn, t("restore_failed"))
201 dom.SetAttribute(restoreBtn, "disabled", "")
202 return
203 }
204 // Derive accounts 1..targetIdx so the requested identity is available.
205 dom.SetTextContent(restoreBtn, t("deriving_n")+" "+itoa(targetIdx)+"...")
206 restoreDeriveUpTo(targetIdx, 1, func() {
207 renderSignerUI()
208 })
209 })
210 })
211 dom.AddEventListener(restoreBtn, "click", restoreCB)
212 dom.AppendChild(signerContent, restoreBtn)
213
214 // --- Import vault file ---
215 sep2 := dom.CreateElement("hr")
216 dom.SetAttribute(sep2, "class", "signer-sep")
217 dom.AppendChild(signerContent, sep2)
218
219 h3b := dom.CreateElement("h3")
220 dom.SetTextContent(h3b, t("import_vault"))
221 dom.AppendChild(signerContent, h3b)
222
223 importBtn := dom.CreateElement("button")
224 dom.SetAttribute(importBtn, "class", "signer-btn signer-btn-secondary")
225 dom.SetTextContent(importBtn, t("choose_vault"))
226 importCB := dom.RegisterCallback(func() {
227 dom.PickFileText(".json", func(data string) {
228 if data == "" {
229 return
230 }
231 dom.SetTextContent(importBtn, t("restoring"))
232 dom.SetAttribute(importBtn, "disabled", "true")
233 signer.ImportVault(data, func(ok bool) {
234 if ok {
235 renderSignerUI()
236 } else {
237 dom.SetTextContent(importBtn, t("invalid_vault"))
238 dom.SetAttribute(importBtn, "disabled", "")
239 }
240 })
241 })
242 })
243 dom.AddEventListener(importBtn, "click", importCB)
244 dom.AppendChild(signerContent, importBtn)
245 }
246
247 // showMnemonicReveal displays the generated mnemonic for the user to write down.
248 func showMnemonicReveal(mnemonic string) {
249 dom.SetTextContent(signerContent, "")
250
251 h3 := dom.CreateElement("h3")
252 dom.SetTextContent(h3, t("write_seed"))
253 dom.AppendChild(signerContent, h3)
254
255 warn := dom.CreateElement("p")
256 dom.SetAttribute(warn, "class", "signer-warn")
257 dom.SetTextContent(warn, t("seed_warning"))
258 dom.AppendChild(signerContent, warn)
259
260 mnBox := dom.CreateElement("div")
261 dom.SetAttribute(mnBox, "class", "signer-mnemonic")
262
263 words := splitMnemonicWords(mnemonic)
264 for i, w := range words {
265 span := dom.CreateElement("span")
266 dom.SetAttribute(span, "class", "signer-word")
267 dom.SetTextContent(span, itoa(i+1)+". "+w)
268 dom.AppendChild(mnBox, span)
269 }
270 dom.AppendChild(signerContent, mnBox)
271
272 copyBtn := dom.CreateElement("button")
273 dom.SetAttribute(copyBtn, "class", "signer-btn signer-btn-secondary")
274 dom.SetAttribute(copyBtn, "data-mn", mnemonic)
275 dom.SetTextContent(copyBtn, t("copy_clipboard"))
276 dom.SetAttribute(copyBtn, "data-label", t("copy_clipboard"))
277 dom.SetAttribute(copyBtn, "data-copied", t("copied"))
278 dom.SetAttribute(copyBtn, "onclick", "var b=this;navigator.clipboard.writeText(b.dataset.mn).then(function(){b.textContent=b.dataset.copied});setTimeout(function(){b.textContent=b.dataset.label},1500)")
279 dom.AppendChild(signerContent, copyBtn)
280
281 doneBtn := dom.CreateElement("button")
282 dom.SetAttribute(doneBtn, "class", "signer-btn")
283 dom.SetTextContent(doneBtn, t("saved_it"))
284 doneCB := dom.RegisterCallback(func() {
285 renderSignerUI()
286 })
287 dom.AddEventListener(doneBtn, "click", doneCB)
288 dom.AppendChild(signerContent, doneBtn)
289 }
290
291 func splitMnemonicWords(s string) []string {
292 var words []string
293 start := -1
294 for i := 0; i < len(s); i++ {
295 if s[i] == ' ' {
296 if start >= 0 {
297 words = append(words, s[start:i])
298 start = -1
299 }
300 } else if start < 0 {
301 start = i
302 }
303 }
304 if start >= 0 {
305 words = append(words, s[start:])
306 }
307 return words
308 }
309
310
311 func renderUnlockVault() {
312 p := dom.CreateElement("p")
313 dom.SetTextContent(p, t("vault_locked"))
314 dom.AppendChild(signerContent, p)
315
316 input := dom.CreateElement("input")
317 dom.SetAttribute(input, "type", "password")
318 dom.SetAttribute(input, "placeholder", t("password"))
319 dom.SetAttribute(input, "class", "signer-input")
320 dom.AppendChild(signerContent, input)
321 dom.Focus(input)
322
323 btn := dom.CreateElement("button")
324 dom.SetAttribute(btn, "class", "signer-btn")
325 dom.SetTextContent(btn, t("unlock"))
326 cb := dom.RegisterCallback(func() {
327 pw := dom.GetProperty(input, "value")
328 if pw == "" {
329 return
330 }
331 dom.SetTextContent(p, t("deriving_key"))
332 dom.SetAttribute(btn, "disabled", "true")
333 signer.UnlockVault(pw, func(ok bool) {
334 if ok {
335 renderSignerUI()
336 } else {
337 dom.SetTextContent(p, t("wrong_password"))
338 dom.SetAttribute(btn, "disabled", "")
339 }
340 })
341 })
342 dom.AddEventListener(btn, "click", cb)
343 dom.AddEnterKeyListener(input, cb)
344 dom.AppendChild(signerContent, btn)
345 }
346
347 func renderIdentityList() {
348 signer.ListIdentities(func(list string) {
349 signer.IsHD(func(hd bool) {
350 dom.SetTextContent(signerContent, "")
351
352 h := dom.CreateElement("h3")
353 dom.SetTextContent(h, t("identities"))
354 dom.AppendChild(signerContent, h)
355
356 renderIdentitiesFromJSON(list, hd)
357
358 if hd {
359 renderHDControls()
360 } else {
361 renderLegacyAddIdentity()
362 }
363 renderBottomActions(hd)
364 })
365 })
366 }
367
368 func renderHDControls() {
369 addDiv := dom.CreateElement("div")
370 dom.SetAttribute(addDiv, "class", "signer-add")
371
372 nameInput := dom.CreateElement("input")
373 dom.SetAttribute(nameInput, "type", "text")
374 dom.SetAttribute(nameInput, "placeholder", t("nickname_placeholder"))
375 dom.SetAttribute(nameInput, "class", "signer-input")
376 dom.AppendChild(addDiv, nameInput)
377
378 deriveBtn := dom.CreateElement("button")
379 dom.SetAttribute(deriveBtn, "class", "signer-btn")
380 dom.SetTextContent(deriveBtn, t("derive_new"))
381 deriveCB := dom.RegisterCallback(func() {
382 name := dom.GetProperty(nameInput, "value")
383 dom.SetAttribute(deriveBtn, "disabled", "true")
384 dom.SetTextContent(deriveBtn, t("deriving"))
385 signer.DeriveIdentity(name, func(pk string) {
386 if pk == "" {
387 dom.SetAttribute(deriveBtn, "disabled", "")
388 dom.SetTextContent(deriveBtn, t("derive_new"))
389 return
390 }
391 // Stash name — kind 0 will be published after login when relays are live.
392 pendingK0Name = name
393 renderSignerUI()
394 })
395 })
396 dom.AddEventListener(deriveBtn, "click", deriveCB)
397 dom.AppendChild(addDiv, deriveBtn)
398 dom.AppendChild(signerContent, addDiv)
399 }
400
401 func parseAccountIdx(s string) int {
402 n := 0
403 for i := 0; i < len(s); i++ {
404 c := s[i]
405 if c >= '0' && c <= '9' {
406 n = n*10 + int(c-'0')
407 } else {
408 break
409 }
410 }
411 return n
412 }
413
414 // restoreDeriveUpTo derives accounts from cur up to target sequentially.
415 func restoreDeriveUpTo(target, cur int, done func()) {
416 if cur > target {
417 done()
418 return
419 }
420 signer.DeriveIdentity("Identity "+itoa(cur), func(pk string) {
421 restoreDeriveUpTo(target, cur+1, done)
422 })
423 }
424
425 // flushPendingK0 publishes a kind 0 if one was queued during identity derivation.
426 // Called from showApp() after relays are live.
427 func flushPendingK0() {
428 name := pendingK0Name
429 if name == "" || pubhex == "" {
430 return
431 }
432 pendingK0Name = ""
433 publishKind0ForIdentity(pubhex, name, func() {})
434 }
435
436
437 // publishKind0ForIdentity switches to the given identity, signs a kind 0 event
438 // with the nickname, publishes it to all relays, then calls done.
439 func publishKind0ForIdentity(pk, name string, done func()) {
440 signer.SwitchIdentity(pk, func(ok bool) {
441 if !ok {
442 done()
443 return
444 }
445 content := "{\"name\":" + helpers.JsonString(name) + ",\"display_name\":" + helpers.JsonString(name) + "}"
446 ts := dom.NowSeconds()
447 unsigned := "{\"kind\":0,\"content\":" + helpers.JsonString(content) +
448 ",\"tags\":[],\"created_at\":" + i64toa(ts) +
449 ",\"pubkey\":\"" + pk + "\"}"
450 signer.SignEvent(unsigned, func(signed string) {
451 if signed != "" {
452 dom.PostToSW("[\"EVENT\"," + signed + "]")
453 dom.ConsoleLog("published kind 0 for " + pk[:8] + " name=" + name)
454 }
455 done()
456 })
457 })
458 }
459
460 func renderLegacyAddIdentity() {
461 addDiv := dom.CreateElement("div")
462 dom.SetAttribute(addDiv, "class", "signer-add")
463
464 addInput := dom.CreateElement("input")
465 dom.SetAttribute(addInput, "type", "password")
466 dom.SetAttribute(addInput, "placeholder", "nsec1...")
467 dom.SetAttribute(addInput, "class", "signer-input")
468 dom.AppendChild(addDiv, addInput)
469
470 addBtn := dom.CreateElement("button")
471 dom.SetAttribute(addBtn, "class", "signer-btn")
472 dom.SetTextContent(addBtn, t("add"))
473 addCB := dom.RegisterCallback(func() {
474 nsec := dom.GetProperty(addInput, "value")
475 if nsec == "" {
476 return
477 }
478 signer.AddIdentity(nsec, func(ok bool) {
479 if ok {
480 dom.SetProperty(addInput, "value", "")
481 renderSignerUI()
482 }
483 })
484 })
485 dom.AddEventListener(addBtn, "click", addCB)
486 dom.AppendChild(addDiv, addBtn)
487 dom.AppendChild(signerContent, addDiv)
488 }
489
490 func renderBottomActions(hd bool) {
491 actions := dom.CreateElement("div")
492 dom.SetAttribute(actions, "class", "signer-actions")
493
494 // Show seed phrase button (HD only).
495 if hd {
496 seedBtn := dom.CreateElement("button")
497 dom.SetAttribute(seedBtn, "class", "signer-btn signer-btn-secondary")
498 dom.SetTextContent(seedBtn, t("show_seed"))
499 seedCB := dom.RegisterCallback(func() {
500 signer.GetMnemonic(func(m string) {
501 if m != "" {
502 showMnemonicReveal(m)
503 }
504 })
505 })
506 dom.AddEventListener(seedBtn, "click", seedCB)
507 dom.AppendChild(actions, seedBtn)
508 }
509
510 // Export vault button.
511 exportBtn := dom.CreateElement("button")
512 dom.SetAttribute(exportBtn, "class", "signer-btn signer-btn-secondary")
513 dom.SetTextContent(exportBtn, t("export_vault"))
514 exportCB := dom.RegisterCallback(func() {
515 signer.ExportVault(func(data string) {
516 if data == "" {
517 return
518 }
519 dom.DownloadText("smesh-vault.json", data, "application/json")
520 })
521 })
522 dom.AddEventListener(exportBtn, "click", exportCB)
523 dom.AppendChild(actions, exportBtn)
524
525 // Lock vault button.
526 lockBtn := dom.CreateElement("button")
527 dom.SetAttribute(lockBtn, "class", "signer-btn signer-btn-secondary")
528 dom.SetTextContent(lockBtn, t("lock_vault"))
529 lockCB := dom.RegisterCallback(func() {
530 signer.LockVault(func() {
531 renderSignerUI()
532 })
533 })
534 dom.AddEventListener(lockBtn, "click", lockCB)
535 dom.AppendChild(actions, lockBtn)
536
537 // Reset extension button.
538 resetBtn := dom.CreateElement("button")
539 dom.SetAttribute(resetBtn, "class", "signer-btn signer-btn-danger")
540 dom.SetTextContent(resetBtn, t("reset_extension"))
541 resetCB := dom.RegisterCallback(func() {
542 if !dom.Confirm(t("reset_confirm")) {
543 return
544 }
545 signer.ResetExtension(func(ok bool) {
546 if !ok {
547 return
548 }
549 // Verify the vault is actually gone before clearing app state.
550 signer.GetVaultStatus(func(status string) {
551 if status == "none" {
552 localstorage.RemoveItem(lsKeyPubkey)
553 dom.LocationReload()
554 }
555 })
556 })
557 })
558 dom.AddEventListener(resetBtn, "click", resetCB)
559 dom.AppendChild(actions, resetBtn)
560
561 dom.AppendChild(signerContent, actions)
562 }
563
564 func renderIdentitiesFromJSON(listJSON string, hd bool) {
565 // Parse: [{"pubkey":"...","name":"...","active":true}, ...]
566 i := 0
567 idxNum := 0
568 for i < len(listJSON) && listJSON[i] != '[' {
569 i++
570 }
571 i++
572 for i < len(listJSON) {
573 for i < len(listJSON) && listJSON[i] != '{' && listJSON[i] != ']' {
574 i++
575 }
576 if i >= len(listJSON) || listJSON[i] == ']' {
577 break
578 }
579 end := i + 1
580 depth := 1
581 for end < len(listJSON) && depth > 0 {
582 if listJSON[end] == '{' {
583 depth++
584 } else if listJSON[end] == '}' {
585 depth--
586 } else if listJSON[end] == '"' {
587 end++
588 for end < len(listJSON) && listJSON[end] != '"' {
589 if listJSON[end] == '\\' {
590 end++
591 }
592 end++
593 }
594 }
595 end++
596 }
597 obj := listJSON[i:end]
598 pk := helpers.JsonGetString(obj, "pubkey")
599 name := helpers.JsonGetString(obj, "name")
600 if pk == "" {
601 i = end
602 idxNum++
603 continue
604 }
605
606 row := dom.CreateElement("div")
607 dom.SetAttribute(row, "class", "signer-identity")
608
609 label := dom.CreateElement("span")
610 display := pk[:8] + "..."
611 if name != "" {
612 display = name + " (" + pk[:8] + "...)"
613 }
614 if hd {
615 display = "#" + itoa(idxNum) + " " + display
616 }
617 dom.SetTextContent(label, display)
618 dom.AppendChild(row, label)
619
620 btns := dom.CreateElement("span")
621
622 // HD identity 0 is the seed — skip it entirely.
623 if hd && idxNum == 0 {
624 i = end
625 idxNum++
626 continue
627 }
628
629 // Switch button.
630 switchBtn := dom.CreateElement("button")
631 dom.SetAttribute(switchBtn, "class", "signer-btn-sm")
632 dom.SetTextContent(switchBtn, t("use"))
633 switchPK := pk
634 switchCB := dom.RegisterCallback(func() {
635 signer.SwitchIdentity(switchPK, func(ok bool) {
636 if ok {
637 hideSignerModal()
638 if pubkey == nil {
639 pubhex = switchPK
640 pubkey = helpers.HexDecode(switchPK)
641 localstorage.SetItem(lsKeyPubkey, pubhex)
642 clearChildren(root)
643 showApp()
644 }
645 }
646 })
647 })
648 dom.AddEventListener(switchBtn, "click", switchCB)
649 dom.AppendChild(btns, switchBtn)
650
651 // Publish kind 0 button.
652 pubBtn := dom.CreateElement("button")
653 dom.SetAttribute(pubBtn, "class", "signer-btn-sm")
654 dom.SetTextContent(pubBtn, t("publish"))
655 pubPK := pk
656 pubName := name
657 pubCB := dom.RegisterCallback(func() {
658 dom.SetTextContent(pubBtn, "...")
659 dom.SetAttribute(pubBtn, "disabled", "true")
660 publishKind0ForIdentity(pubPK, pubName, func() {
661 dom.SetTextContent(pubBtn, t("publish"))
662 dom.SetAttribute(pubBtn, "disabled", "")
663 })
664 })
665 dom.AddEventListener(pubBtn, "click", pubCB)
666 dom.AppendChild(btns, pubBtn)
667
668 // Remove button.
669 rmBtn := dom.CreateElement("button")
670 dom.SetAttribute(rmBtn, "class", "signer-btn-sm signer-btn-danger")
671 dom.SetTextContent(rmBtn, "\u00d7")
672 rmPK := pk
673 rmCB := dom.RegisterCallback(func() {
674 signer.RemoveIdentity(rmPK, func(ok bool) {
675 if ok {
676 renderSignerUI()
677 }
678 })
679 })
680 dom.AddEventListener(rmBtn, "click", rmCB)
681 dom.AppendChild(btns, rmBtn)
682
683 dom.AppendChild(row, btns)
684 dom.AppendChild(signerContent, row)
685 i = end
686 idxNum++
687 }
688 }
689