Skip to content

Commit 5c118ad

Browse files
authored
feat(mask): fw-6767 add firefly desktop sync for twitter cookies (#12340)
1 parent dfc5c21 commit 5c118ad

File tree

23 files changed

+941
-24
lines changed

23 files changed

+941
-24
lines changed
Lines changed: 4 additions & 0 deletions
Loading

packages/icons/icon-generated-as-jsx.js

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/icons/icon-generated-as-url.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mask/background/services/helper/firefly.ts

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
1-
import { PersistentStorages } from '@masknet/shared-base'
1+
import { fromHex, toHex, PersistentStorages } from '@masknet/shared-base'
2+
3+
const FIREFLY_ROOT_URL = 'https://firefly.social'
4+
const FIREFLY_API_URL = 'https://api.firefly.land'
5+
const APP_LOGIN_ENCRYPT_IV = '0x4f05c37c16c801c2516b0338a8fd0cf9'
6+
7+
// Types for desktop sync
8+
export interface SocialAccountTwitter {
9+
type: 'x' // SourceInURL.X
10+
user_id: string
11+
handle: string
12+
consumerKey: string
13+
consumerKeySecret: string
14+
accessToken: string
15+
accessTokenSecret: string
16+
cookie?: string
17+
}
18+
19+
export interface TwitterOAuthData {
20+
oauth_token: string
21+
oauth_token_secret: string
22+
user_id: string
23+
screen_name: string
24+
}
225

326
export async function loginFireflyViaTwitter() {
427
const data = await browser.storage.local.get('firefly_x_oauth')
@@ -18,7 +41,7 @@ export async function loginFireflyViaTwitter() {
1841
const json = await res.json()
1942
if (!json.success) throw new Error(json.message)
2043

21-
const res2 = await fetch('https://api.firefly.land/v3/auth/exchange/twitter', {
44+
const res2 = await fetch(`${FIREFLY_API_URL}/v3/auth/exchange/twitter`, {
2245
method: 'POST',
2346
headers: {
2447
'Content-Type': 'application/json',
@@ -32,3 +55,135 @@ export async function loginFireflyViaTwitter() {
3255
await PersistentStorages.Settings.storage.firefly_account.setValue(json2.data)
3356
return json2.data
3457
}
58+
59+
// Desktop sync functions
60+
export async function encrypt(plainText: string, cryptoKey: string): Promise<string> {
61+
const iv = fromHex(APP_LOGIN_ENCRYPT_IV)
62+
const cryptoBytes = new TextEncoder().encode(cryptoKey)
63+
const hashBuffer = await crypto.subtle.digest('SHA-256', cryptoBytes)
64+
const aesKey = await crypto.subtle.importKey('raw', hashBuffer, { name: 'AES-CBC' }, false, ['encrypt'])
65+
66+
const plainBytes = new TextEncoder().encode(plainText)
67+
const encryptedBuffer = await crypto.subtle.encrypt(
68+
{
69+
name: 'AES-CBC',
70+
iv,
71+
},
72+
aesKey,
73+
plainBytes,
74+
)
75+
76+
const hex = toHex(new Uint8Array(encryptedBuffer))
77+
return hex.startsWith('0x') ? hex.slice(2) : hex
78+
}
79+
80+
export interface DesktopLinkInfoResponse {
81+
link: string
82+
session: string
83+
expiresAt: string
84+
}
85+
86+
export async function getDesktopSyncLinkInfo(accessToken: string): Promise<DesktopLinkInfoResponse> {
87+
const url = `${FIREFLY_API_URL}/desktop/sync/linkInfo`
88+
const response = await fetch(url, {
89+
method: 'GET',
90+
headers: {
91+
Authorization: `Bearer ${accessToken}`,
92+
},
93+
})
94+
if (!response.ok) throw new Error(`Failed to get desktop sync link info: ${response.statusText}`)
95+
const json = await response.json()
96+
if (json.code) throw new Error('Failed to get desktop sync link info, code: ' + json.code)
97+
return json.data
98+
}
99+
100+
export enum DesktopSyncChannelStatus {
101+
Pending = 'pending',
102+
Scanned = 'scanned',
103+
Confirmed = 'confirmed',
104+
DataReady = 'dataReady',
105+
Cancel = 'cancel',
106+
Expired = 'expired',
107+
}
108+
109+
export interface SyncChannelStatusResponse {
110+
status: DesktopSyncChannelStatus
111+
}
112+
113+
export async function getSyncChannelStatus(session: string, accessToken: string): Promise<SyncChannelStatusResponse> {
114+
const url = `${FIREFLY_API_URL}/desktop/sync/channelStatus`
115+
const response = await fetch(url, {
116+
method: 'POST',
117+
headers: {
118+
'Content-Type': 'application/json',
119+
Authorization: `Bearer ${accessToken}`,
120+
},
121+
body: JSON.stringify({ session }),
122+
})
123+
if (!response.ok) throw new Error(`Failed to get sync channel status: ${response.statusText}`)
124+
const json = await response.json()
125+
return json.data
126+
}
127+
128+
export type ConfirmSyncChannelOperation = 'confirm' | 'cancel'
129+
130+
export async function confirmSyncChannel(
131+
session: string,
132+
operation: ConfirmSyncChannelOperation,
133+
accessToken: string,
134+
): Promise<{ status: string }> {
135+
const url = `${FIREFLY_API_URL}/desktop/sync/confirm`
136+
const response = await fetch(url, {
137+
method: 'POST',
138+
headers: {
139+
'Content-Type': 'application/json',
140+
Authorization: `Bearer ${accessToken}`,
141+
},
142+
body: JSON.stringify({ session, op: operation }),
143+
})
144+
if (!response.ok) throw new Error(`Failed to confirm sync channel: ${response.statusText}`)
145+
const json = await response.json()
146+
return json.data
147+
}
148+
149+
export interface TwitterCookiesPayload {
150+
twitterAccounts: SocialAccountTwitter[]
151+
fireflyAccountData: {
152+
firefly_account_token: string
153+
account_id: string
154+
account_uid: string
155+
display_name: string
156+
avatar: string
157+
}
158+
}
159+
160+
// Helper function to get Twitter OAuth data from storage
161+
export async function getTwitterOAuthData(): Promise<TwitterOAuthData | null> {
162+
const data = await browser.storage.local.get('firefly_x_oauth')
163+
if (!data?.firefly_x_oauth) return null
164+
return data.firefly_x_oauth as TwitterOAuthData
165+
}
166+
167+
export async function syncTwitterCookies(
168+
session: string,
169+
cryptoKey: string,
170+
encryptedPayload: string,
171+
accessToken: string,
172+
): Promise<void> {
173+
const url = `${FIREFLY_ROOT_URL}/api/firefly/desktop-sync/upload`
174+
const response = await fetch(url, {
175+
method: 'POST',
176+
headers: {
177+
'Content-Type': 'application/json',
178+
Authorization: `Bearer ${accessToken}`,
179+
},
180+
body: JSON.stringify({
181+
session,
182+
cryptoKey,
183+
encryptedPayload,
184+
}),
185+
})
186+
if (!response.ok) throw new Error(`Failed to sync twitter cookies: ${response.statusText}`)
187+
const json = await response.json()
188+
if (!json.success) throw new Error(json.message)
189+
}

packages/mask/background/services/helper/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,18 @@ export { fetchSandboxedPluginManifest } from './sandboxed.js'
1818
export { getActiveTab } from './tabs.js'
1919
export { requestXOAuthToken, resolveXOAuth, resetXOAuth } from './oauth-x.js'
2020
export { loginFireflyViaTwitter } from './firefly.js'
21+
export {
22+
encrypt,
23+
getDesktopSyncLinkInfo,
24+
getSyncChannelStatus,
25+
confirmSyncChannel,
26+
syncTwitterCookies,
27+
getTwitterOAuthData,
28+
type DesktopLinkInfoResponse,
29+
type SyncChannelStatusResponse,
30+
type TwitterCookiesPayload,
31+
type SocialAccountTwitter,
32+
type TwitterOAuthData,
33+
type ConfirmSyncChannelOperation,
34+
} from './firefly.js'
35+
export { getXOAuthToken } from './oauth-x.js'

packages/mask/popups/components/WalletAvatar/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ const useStyles = makeStyles()((theme) => ({
2222

2323
interface Props extends HTMLProps<HTMLDivElement> {
2424
size?: number
25+
badgeSize?: number
2526
address: string
2627
}
27-
export const WalletAvatar = memo<Props>(function WalletAvatar({ size = 30, address, ...rest }) {
28+
export const WalletAvatar = memo<Props>(function WalletAvatar({ size = 30, address, badgeSize = 12, ...rest }) {
2829
const { classes, cx } = useStyles()
2930
const { wallets: fireflyWallets } = useWallets()
3031

@@ -38,7 +39,7 @@ export const WalletAvatar = memo<Props>(function WalletAvatar({ size = 30, addre
3839
return (
3940
<div {...rest} className={cx(classes.container, rest.className)}>
4041
<Image size={size} src={fireflyAccount.avatar} rounded />
41-
<Icons.Firefly className={classes.badgeIcon} size={12} />
42+
<Icons.Firefly className={classes.badgeIcon} size={badgeSize} />
4243
</div>
4344
)
4445
return <Icons.MaskBlue size={size} className={rest.className} />

packages/mask/popups/pages/Wallet/Receive/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1+
import { Trans, useLingui } from '@lingui/react/macro'
12
import { CopyButton, FormattedAddress, Icon, ImageIcon, NetworkIcon, TokenIcon } from '@masknet/shared'
23
import { NetworkPluginID } from '@masknet/shared-base'
34
import { makeStyles } from '@masknet/theme'
45
import { useChainContext, useNetworks } from '@masknet/web3-hooks-base'
5-
import { type ChainId, formatEthereumAddress } from '@masknet/web3-shared-evm'
6+
import { formatEthereumAddress, type ChainId } from '@masknet/web3-shared-evm'
67
import { Box, Skeleton, Typography, type AvatarProps } from '@mui/material'
78
import { memo } from 'react'
89
import { QRCode } from 'react-qrcode-logo'
910
import { useTitle, useTokenParams } from '../../../hooks/index.js'
1011
import { useAsset } from '../hooks/useAsset.js'
11-
import { Trans, useLingui } from '@lingui/react/macro'
1212

1313
const useStyles = makeStyles()((theme) => {
1414
const isDark = theme.palette.mode === 'dark'

0 commit comments

Comments
 (0)