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