# Authc & Wallet 技术文档

## 项目地址

- [Authc SDK](https://github.com/comsand/authc-spa-js)
- [Authc 客户端/服务端](https://github.com/comsand/authc.io)
- [Wallet 客户端](https://github.com/comsand/authc-wallet-frontend)

## Authc SDK

> 这里主要介绍的是 authc-spa-js 的技术实现，authc.io 相关不在这里介绍

### authc-spa-js 主要包含如下入口

1. **authc-spa-js** 主要负责登录方面的工作，如 email, username/password, 以及第三方登录,如 google, facebook 等
2. **authc-spa-js/wallet** 负责钱包相关登录，如 metamask，walletconnect 以及我们内部的钱包登录
3. **authc-spa-js/authc** 主要负责 authc + wallet 的组合登录
4. **autch-spa-js/iframe** 负责处理 iframe 登录后重定向回来的地址上的 code 和 state

### authc-spa-js

#### 使用方式

> 这里只介绍我们最常用的 loginWithIframe 登录

```ts
// 1. 初始化
const authc = createAuthcClient({
  domain: 'ucollex.stage.authc.io',
  client_id: '95f15921-52f4-47dc-985b-d50ecf7cddcc',
  useRefreshTokens: true,
  cacheLocation: 'localstorage',
  redirect_uri: window.origin,
  advancedOptions: {
    defaultScope: 'openid profile email phone',
  },
  walletOptions,
})

// 2. 使用 loginWithIframe 进行登录
authc.then(ins => ins.loginWithIframe({
  authorization_endpoint: 'oauth/authorize',
  response_mode: 'fragment',
  loading: false,
})).then(() => {
  // 登录成功
}).catch((err) => {
  // 登录失败
})

// 这样我们就完成了登录
```

#### 实现原理

当执行 loginWithIframe 时会在当前页面创建一个 iframe，并让这个 iframe 打开我们实际登录的地址并监听 message 的消息，然后等待用户完成登录后，iframe 会重定向到我们指定的 redirect_uri。


当成功登录后重定向过来的地址就像这样:

```
https://domain.com/#code=xxx&state=xxx
```

然后我们的 iframe 需要执行 authc 的 `handleRedirectCallback` 方法解析出来 code 和 state。

然后通过 `postMessage` 传递给父页面就像这样：

```ts
const target = window.parent.opener || window.parent
target.postMessage(
  {
    type: 'authorization_response',
    response: {
      code,
      state,
    },
  },
  '*',
)
```

此时，父页面会判断 state 是否一致，如果一致就会执行 `handleRedirectCallback` 方法，这样就完成了登录。这一次执行 `handleRedirectCallback` 的主要是用 code 换取 token，并把结果写入到 localStorage(取决于你设置的 `cacheLocation` 配置)，等拿到 token 之后会移除 iframe。结束 `loginWithIframe`。

#### 例子

- [examples/login](examples/login)

### authc-spa-js/iframe

这里的主要功能就是帮我们执行 handleRedirectCallback 的方法，我们只需要在的指定的重定向地址页面引入它，就会自动解析 url 取出 code 和 state 并向父页面 postMessage 传递 code 和 state。

#### 使用方式

> 注意：这个是需要设置为 vite 的入口文件之一，因为使用到了 import 的方式，打包才能在浏览器中使用

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Login iframe</title>
  </head>
  <body>
    <script type="module">
      import "authc-spa-js/iframe";
    </script>
  </body>
</html>

```


### Why we use this?

在这之前我们通常是把重定向地址设置为  window.origin，当我们完成登录后会重定向到我们的首页。

这导致了以下的问题：

1. 执行两遍 handleRedirectCallback 方法
2. 登录加载太慢，因为首页的内容也要加载
3. 首页的内容覆盖在上面

所以我们需要把重定向地址设置为一个空白页面，然后在这个页面引入 authc-spa-js/iframe，这样就不会有上面的问题了。

#### 例子

- [examples/authc](examples/authc)


## Wallet & Wallet SDK

### 技术栈

1. [@tkey/*](https://github.com/tkey/tkey)
2. [@walletconnect/*](https://github.com/WalletConnect/walletconnect-monorepo)
3. [@web3modal/core](https://github.com/web3modal/web3modal)
4. [@wagmi/core](https://github.com/wagmi-dev/wagmi)
5. [ethers](https://github.com/ethers-io/ethers.js)

### Wallet 客户端

#### Authc 面向 web3

在 web3 的世界里，钱包就是一个用户的身份，用户可以通过钱包来签名交易，这样就可以完成一些操作，比如转账，授权等。
并且钱包登录是我们的主要登录方式，为此我们基于 Authc + TKey 实现一个自己的钱包。

#### 线上钱包

1. [Ucollex Wallet](https://wallet.ucollex.io/)
2. [MADworld Wallet](https://wallet.madworld.io/)

#### TKey 是什么？

TKey 是 web3auth 的底层实现，所以他可以自建一个 [Web3Auth](https://web3auth.io/) 平台。这 `web3auth` 正是我们需要的。

它可以支持你用第三方登录如，google, facebook 进行登录，也可以支持你用钱包登录，如 metamask, walletconnect。当你使用第三方登录时，会自动为你创建一个 web3 钱包。然后你就可以用你的第三方账号来管理你的钱包。

#### 为什么使用 TKey？

我们已经有一个 Authc 的平台了，已经可以完成 web3auth 的 auth 部分，我们只需要 web3 的部分，然后连通起来。我们就可以为我们的 Authc 用户创建一个钱包了。

#### 怎么做？

我们在 authc-spa-js 里添加了 TKey 的实现，当初始化 Authc 时，如果传递了 `tkeyOptions`，就会自动创建一个我们封装好的 TKey 实例，然后赋值给了 tk。也就是说当我们访问 authc.tk 的时候，就是访问的 TKey 实例。

`tk` 的实现主要是为了方便我们使用，和 authc 打通，把整体的流程封装成了几个方法，然后简化一下常用的方法。方便在 wallet 端接入使用。

这里重点介绍下 `triggerLogin` 和 `reconstruct` 方法

#### triggerLogin

这是和 Authc 打通的关键一步。当我们完成了 Authc 登录，我们将会调用 TKey 的 `triggerLogin` 方法。

这个方法是让 TKey 登录。那为什么我们登录完 Authc 还要登录 TKey 呢？

因为 TKey 才是真正创建钱包的地方，我们需要把 Authc 的用户和 TKey 的用户关联起来，这样我们才能在 Authc 的用户下创建一个 TKey 的钱包。

这是 triggerLogin 的核心代码

```ts
const tokens = await this.authc.getTokenSilently({ detailedResponse: true, ignoreCache: true })
return this.serviceProvider.triggerLogin({
  typeOfLogin: 'jwt',
  verifier: this.options.verifier,
  clientId: 'DO_NOT_NEED',
  jwtParams: {
    verifierIdField: 'openid',
    domain: `https://${this.authc.options.domain}`,
    id_token: tokens.id_token,
  },
})
```

这里的 [verifier](https://dashboard.web3auth.io/home/customauth) 属性，我们需要去 web3auth 的 dashboard 创建。
这里我们用的 jwt 的登录方式，所以创建时也需要配置相对应的规则。

这里的 `jwtParams` 就是自己设定规则，`id_token` 就是 web3auth 用于解析的

当我们登录完后，我们还需要判断用户是否已经创建了钱包，如果没有创建，我们要让他跳转到创建钱包的页面，如果已经创建了，我们会让他跳转到重建页面

TKey 是没有提供方法告诉我们该用户是没有创建的，这里我们使用了 `TKey.initialize` 来实现，代码如下：

```ts
try {
  await this.thresholdKey.initialize({ neverInitializeNewKey: true })
  //  有钱包
}
catch {
  // 没有钱包 返回 301 状态码
}
```

成功登录后只是第一步，我们将自动执行 `reconstruct` 方法。

#### reconstruct

reconstruct 用于是重建我们的钱包，我们通常需要 2/3 的 share 才能重建成功，而 Authc 的登录是我们的其中一个 share。

For a 2 out of 3 (2/3) setup, we give the user three shares: ShareA, ShareB, and ShareC.

- ShareA is managed and split across Web3Auth's Auth Network, accessed by an OAuth login provider that a user owns. For example, a user could use their Google account to access their share.
- ShareB is stored on the user’s device: The implementation is device and system specific. For example, on mobile devices, this share could be stored in device storage secured via biometrics.
- ShareC is a recovery share: An additional share to be kept by the user, possibly kept on a separate device, downloaded, or based on user input with enough entropy (e.g., password, security questions, hardware device, etc.).

上面就是 TKey 的官方文档对于 Share 的介绍，我们可以看到，我们的 Authc 登录就是 ShareA，我们还需要 ShareB 和 ShareC。

我们在为其创建钱包时，会询问他是否需要设置 password 或者设置 recovery email 等等，这其实都是在配置 Share B。而 Share C 则是设备的 share，我们会把他存储在 localStorage 里并提供下载功能。

reconstruct 的主要逻辑就是，对传进来的参数进行填充验证，如果验证成功，就会执行 `TKey.reconstruct` 方法。当如果用户没有 device share 时则会为他为创建一个 device share 并且存储在 `localStorage` 里。（这个能力来自 `@tkey/web-storage` 模块）然后下次登录时如果 `localStorage` 里还有这个 share 则不会再进到重建页面，而是会到成功解锁后到钱包展示页面。

登录流程如下

1. 登录 Authc
2. 执行 `authc.triggerLogin` 方法
3. 如果还不满足 2/3 的条件，则跳转到解锁页面
4. 满足 2/3 条件后，执行 `authc.reconstruct` 方法
5. 如果没有 device share，则创建一个 device share 并存储在 `localStorage` 里
6. 跳转到钱包展示页面

#### 钱包签名/交易

在成功解锁钱包后，我们可能需要处理如 `sign`, `sendTransaction` 等等的操作，这些操作都是需要用户确认的，但我们现在只是创建了一个钱包。

为此，我们采用使用 `Walletconnect` 来监听这些需要用户处理的消息，我们提供了界面让用户来确认, 可以 Approve 或 Reject。

具体怎么做？

我们已经在客户端新增了一个 wc 的页面，该页面接收一个 uri 的参数。然后我们会自动地去连接这个 uri，然后监听 `Walletconnect` 的消息。

#### 总结

这里我们可以看到，我们的登录流程其实是和 Authc 无关的，我们只是利用了 Authc 的登录来作为我们的 ShareA，而 ShareB 和 ShareC 则是由我们自己来创建的。

我们完成登录后，我们会自动去连接 `Walletconnect`，然后监听 `Walletconnect` 的消息。弹出消息确认框，用户确认后，我们会执行相应的操作。

### authc-spa-js/wallet

主要目的是用于支持我们自己的钱包登录，以及支持第三方钱包登录。

#### 使用方式

```ts
const walletOptions: AuthcWalletOptions = {
  projectId: '2cca1d25030c9d4b87b8e64ca7a91ad5',
  customWallets: [
    {
      links: {
        universal: 'https://wallet.madworld.io'
      },
      name: 'Madworld',
      id: 'madworld',
    },
  ],
  defaultChain: { id: 137 } as any,
  version: 1,
}
const wallet = new AuthcWalletLogin(walletOptions)

// 用 postMessage 连接 metamask
window.postMessage({
  type: 'authcWallet',
  data: {
    type: 'connectWalletByWalletName',
    name: 'metamask',
  },
})

// 使用 connectWalletByWalletName 连接 coinbase
wallet.connectWalletByWalletName('coinbase').then((wallet) => {
  // 连接成功
})
//

// 使用 wallet.connectWalletCustom 连接 madwrold 钱包
wallet.connectWalletCustom('madworld').then((res) => {
  // 连接成功
})
```

当需要连接我们内部自己实现的钱包时，我们需要配置 customWallets 然后把钱包的地址和名字添加进去，然后就可以使用 connectWalletCustom 连接了。

#### 实现原理

> 这里主要讲解我们是怎么实现连接自己的钱包的

这里主要用到了 `web3modal` 和 `wagmi` 两个库。去连通我们的钱包。

怎么做？

当我们要连接自己的钱包时，我们会在 `document.body` 插入一个 `iframe`，然后 `iframe` 的地址则是我们钱包的地址并拼接了 `uri`。地址如下：

```
http://localhost:5173/wc?uri=wc%253Ad9693464-8f76-458c-bad5-7320744c9dd0%25401%253Fbridge%253Dhttps%25253A%25252F%25252Fi.bridge.walletconnect.org%2526key%253D7164d30e2d4ad98b1117c4d5e6cc76e8e3fdb7543714f36c790bf9787167fef0&automatic=1
```

当我们的钱包客户端加载完成后，会检查 `uri` 参数，然后使用 `Walletconnect` 消费这个 `uri`。连接成功后将会弹出一个 proposal 弹窗，让您选择你需要连接的钱包并最终完成连接。 

当有 `automatic` 参数时，地址上的 `uri` 并不是最终消费的 `uri`。我们会调用 `postMessage({ type: 'autchWallet', data: { type: 'wcUri'} })` 获取新的 `uri` 并消费，然后跳过 proposal 弹窗，选择默认的钱包完成连接。

当我们已经连通 `Walletconnect` 后，我们就可以使用该 SDK 提供的方法进行发起消息如 `sign`, `sendTransaction` 等等。

#### 例子

- [examples/web3modal](examples/web3modal)
- [codepen demo](​https://codepen.io/Qingggggggg/pen/BaPyRqB)

### authc-spa-js/authc

#### 背景

我们在 `POC` 的项目里，我们的登录弹窗里有 web2 的登录方式也有 web3 的登录方式。当用户选择 web2 的登录方式实际上是使用了我们自己内部实现的钱包。当用户选择 web3 的登录方式时，我们会使用 `authc-spa-js/wallet` 来连接第三方钱包。

又因为 `Authc` 的客户端并不能直接使用 `authc-spa-js/wallet` (因为是 iframe 下登录的，登录完就会销毁)，所以 `Authc` 客户端如果要想连接第三方钱，需要使用 `postMessage` 传递消息来让 `POC` 客户端触发对应的登录（这在 `authc-spa-js/wallet` 里面实现的）。

在这种需求背景下，所以我们需要把 `authc-spa-js/wallet` 和 `authc-spa-js` 组合起来使用，并提供了一个新的方法 `loginWithWallet` 来实现这个需求。

#### 使用方式

按照 authc-spa-js 的用法，把 `loginWithIframe` 替换成 `loginWithWallet` 即可

#### 实现原理

当我们调用 `loginWithWallet` 时，其实我们实际调用的也是 loginWithIframe，只是我们在 `loginWithIframe` 里面做了一些处理。

当调用 `loginWithIframe` 我们会打开一个 `Authc` 客户端的 `iframe`，等到  `loginWithIframe` 完成后我们会检查此时 `wallet` 是否已经连接上，如果没连上，则代表是 web2 登录，我们将会打开我们内部的钱包并完成钱包流程。如果连接上了，则我们是收到 postMessage 的登录请求后就会把签名后的消息传递回发消息过来的 `iframe`（这需要和 Authc 客户端协商后该如何去做）。这其实是 `Authc` 用我们的签名信息完成了 `Authc` 登录，但其实这时我们的钱包就已经登录了。所以我们可以判断出来是 `web3` 登录。

## 总结

我们在 authc-spa-js 把 Authc 和 Wallet 打通。让我们的 Authc 可以快速接入第三方钱包。并且我们可以使用我们自己的钱包。