mirror of
https://github.com/luxwallet/connect.git
synced 2026-10-30 19:32:42 +00:00
TS verifiers: EVM, Solana, TON (ton_proof), XRP (secp256k1+ed25519), Bitcoin
(legacy + BIP-322 simple P2WPKH/P2TR) — 61 tests green, typecheck clean.
Go port (go/walletconnect): EVM + Solana via github.com/luxfi/crypto (NO
go-ethereum/ava-labs), TON/XRP/Bitcoin stubs fail-closed — 23 tests green.
IAM integration plan + drafts (integrations/iam/): canonical /v1/iam/web3/
{nonce,verify}, single-use nonce store, multi-wallet WalletLink. Found current
IAM web3 login verifies NO signature (security hole this closes).
159 lines
5.5 KiB
Plaintext
159 lines
5.5 KiB
Plaintext
// DRAFT — destination: github.com/hanzoai/iam/object/web3_store.go
|
|
//
|
|
// Persistence for multi-chain wallet login:
|
|
// - Web3Nonce : single-use, expiring login challenges (anti-replay).
|
|
// - WalletLink: the (chain,address) -> user mapping (one user, N wallets).
|
|
//
|
|
// Register both in object/ormer table sync (the slice passed to
|
|
// ormer.Engine.Sync2(...) / the AutoMigrate list). DO NOT apply yet.
|
|
|
|
package object
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/hanzoai/iam/util"
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Web3Nonce — single-use login nonce.
|
|
//
|
|
// Minted by GET /v1/iam/web3/nonce, burned by POST /v1/iam/web3/verify. The burn
|
|
// is a conditional UPDATE (used=false -> used=true) so two concurrent verifies of
|
|
// the same captured proof cannot both win: exactly one UPDATE affects a row.
|
|
// ----------------------------------------------------------------------------
|
|
|
|
type Web3Nonce struct {
|
|
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
|
Name string `xorm:"varchar(100) notnull pk" json:"name"` // the nonce string itself
|
|
Chain string `xorm:"varchar(20)" json:"chain"`
|
|
Address string `xorm:"varchar(128)" json:"address"`
|
|
Domain string `xorm:"varchar(128)" json:"domain"`
|
|
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
|
ExpireTime string `xorm:"varchar(100)" json:"expireTime"`
|
|
Used bool `xorm:"bool" json:"used"`
|
|
}
|
|
|
|
func AddWeb3Nonce(n *Web3Nonce) (bool, error) {
|
|
affected, err := ormer.Engine.Insert(n)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return affected != 0, nil
|
|
}
|
|
|
|
// BurnWeb3Nonce atomically consumes a nonce. Returns the row iff it existed, was
|
|
// unused, and is unexpired; otherwise (nil, nil). The conditional UPDATE is the
|
|
// replay guard — never split "read then write".
|
|
func BurnWeb3Nonce(nonce string) (*Web3Nonce, error) {
|
|
rec := &Web3Nonce{}
|
|
existed, err := ormer.Engine.Where("name = ?", nonce).Get(rec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !existed || rec.Used {
|
|
return nil, nil
|
|
}
|
|
if exp, err := time.Parse(time.RFC3339, rec.ExpireTime); err == nil {
|
|
if time.Now().After(exp) {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// Conditional, single-row UPDATE: used=false -> true. affected==0 means
|
|
// another request already burned it (lost the race) -> treat as invalid.
|
|
affected, err := ormer.Engine.
|
|
Where("owner = ? AND name = ? AND used = ?", rec.Owner, rec.Name, false).
|
|
Cols("used").
|
|
Update(&Web3Nonce{Used: true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if affected == 0 {
|
|
return nil, nil
|
|
}
|
|
rec.Used = true
|
|
return rec, nil
|
|
}
|
|
|
|
// PurgeExpiredWeb3Nonces is a janitor for a periodic task (optional).
|
|
func PurgeExpiredWeb3Nonces() (int64, error) {
|
|
return ormer.Engine.Where("expire_time < ?", time.Now().Format(time.RFC3339)).Delete(&Web3Nonce{})
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// WalletLink — the (chain,address) -> user binding.
|
|
//
|
|
// One user can own many links (EVM + Solana + BTC + TON + XRP, and several
|
|
// addresses per chain). The (chain,address) pair is globally unique, so a given
|
|
// wallet can be bound to at most one identity — this is what stops a wallet from
|
|
// hijacking someone else's account.
|
|
//
|
|
// Why a side table and not user columns: the legacy model stored one address in
|
|
// one varchar per provider (user.web3onboard). That cannot represent N wallets on
|
|
// M chains. Values, not places: a link is its own value; the user is referenced,
|
|
// not mutated.
|
|
// ----------------------------------------------------------------------------
|
|
|
|
type WalletLink struct {
|
|
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` // organization
|
|
User string `xorm:"varchar(100) notnull pk" json:"user"` // user.Name
|
|
Chain string `xorm:"varchar(20) notnull pk" json:"chain"`
|
|
Address string `xorm:"varchar(128) notnull pk index" json:"address"`
|
|
Scheme string `xorm:"varchar(40)" json:"scheme"`
|
|
PublicKey string `xorm:"varchar(256)" json:"publicKey"`
|
|
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
|
}
|
|
|
|
// GetWalletLink resolves a verified (chain,address) to its link row, scoped to the
|
|
// application's organization. Returns (nil, nil) when no wallet is on file.
|
|
//
|
|
// NOTE: address is compared as the verifier canonicalizes it. For EVM the verifier
|
|
// returns a lowercased 0x address, so store + look up lowercased (do the
|
|
// ToLower in the caller or here) to keep EVM case-insensitive.
|
|
func GetWalletLink(organization, chain, address string) (*WalletLink, error) {
|
|
if chain == "" || address == "" {
|
|
return nil, nil
|
|
}
|
|
link := &WalletLink{}
|
|
existed, err := ormer.Engine.
|
|
Where("owner = ? AND chain = ? AND address = ?", organization, chain, address).
|
|
Get(link)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !existed {
|
|
return nil, nil
|
|
}
|
|
return link, nil
|
|
}
|
|
|
|
func AddWalletLink(link *WalletLink) (bool, error) {
|
|
if link.CreatedTime == "" {
|
|
link.CreatedTime = util.GetCurrentTime()
|
|
}
|
|
affected, err := ormer.Engine.Insert(link)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return affected != 0, nil
|
|
}
|
|
|
|
func DeleteWalletLink(link *WalletLink) (bool, error) {
|
|
affected, err := ormer.Engine.
|
|
Where("owner = ? AND user = ? AND chain = ? AND address = ?",
|
|
link.Owner, link.User, link.Chain, link.Address).
|
|
Delete(&WalletLink{})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return affected != 0, nil
|
|
}
|
|
|
|
// GetWalletLinksByUser lists every wallet a user has linked (for account UI).
|
|
func GetWalletLinksByUser(owner, user string) ([]*WalletLink, error) {
|
|
links := []*WalletLink{}
|
|
err := ormer.Engine.Where("owner = ? AND user = ?", owner, user).Find(&links)
|
|
return links, err
|
|
}
|