mirror of
https://github.com/andrei0x309/clear-wallet.git
synced 2024-12-26 12:10:46 +00:00
commit
d4e93f5f87
51
package.json
51
package.json
@ -16,39 +16,38 @@
|
||||
"pub": "yarn build && yarn release && yarn tsx ./release-scripts/create-release.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ionic/vue": "^7.8.6",
|
||||
"@ionic/vue-router": "^7.8.6",
|
||||
"core-js": "^3.37.1",
|
||||
"ethers": "^6.13.1",
|
||||
"vue": "^3.4.29",
|
||||
"vue-router": "^4.3.3"
|
||||
"@ionic/vue": "^8.2.6",
|
||||
"@ionic/vue-router": "^8.2.6",
|
||||
"core-js": "^3.38.0",
|
||||
"ethers": "^6.13.2",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "^5.2.3",
|
||||
"@crxjs/vite-plugin": "^2.0.0-beta.23",
|
||||
"@types/archiver": "^5.3.4",
|
||||
"@types/chrome": "^0.0.268",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/node": "^20.14.5",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.0",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@crxjs/vite-plugin": "2.0.0-beta.25",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/chrome": "^0.0.269",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^22.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-vue": "^5.1.2",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"archiver": "^5.3.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"archiver": "^7.0.1",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"http-browserify": "^1.7.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"jest": "^29.6.2",
|
||||
"sass": "^1.77.6",
|
||||
"jest": "^29.7.0",
|
||||
"sass": "^1.77.8",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tsx": "^4.15.6",
|
||||
"typescript": "^5.4.5",
|
||||
"ts-jest": "^29.2.4",
|
||||
"tsx": "^4.17.0",
|
||||
"typescript": "^5.5.4",
|
||||
"util": "^0.12.5",
|
||||
"vite": "^5.3.1",
|
||||
"vue-tsc": "^2.0.21",
|
||||
"yarn-upgrade-all": "^0.7.2"
|
||||
"vite": "^5.4.0",
|
||||
"vue-tsc": "^2.0.29",
|
||||
"yarn-upgrade-all": "^0.7.4"
|
||||
},
|
||||
"disabledNativeDependencies": {
|
||||
"@capacitor/app": "^5.0.6",
|
||||
|
@ -3,8 +3,8 @@
|
||||
"name": "__MSG_appName__",
|
||||
"description": "__MSG_appDesc__",
|
||||
"default_locale": "en",
|
||||
"version": "1.4.0",
|
||||
"version_name": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"version_name": "1.4.1",
|
||||
"icons": {
|
||||
"16": "assets/extension-icon/wallet_16.png",
|
||||
"32": "assets/extension-icon/wallet_32.png",
|
||||
|
@ -32,6 +32,10 @@ const routes: Array<RouteRecordRaw> = [
|
||||
path: '/request-network/:rid/:param',
|
||||
component: () => import('@/views/RequestNetwork.vue'),
|
||||
},
|
||||
{
|
||||
path: '/farcaster-actions',
|
||||
component: () => import('@/views/FarcasterActions.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tabs/',
|
||||
component: AppTabs,
|
||||
|
21
src/utils/abis.ts
Normal file
21
src/utils/abis.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export const FARCASTER_PARTIAL_KEY_ABI = [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "idOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "fid",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
159
src/utils/farcaster.ts
Normal file
159
src/utils/farcaster.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { signMsg, getSelectedAddress, getOptimismProvider } from './wallet'
|
||||
import { FARCASTER_PARTIAL_KEY_ABI } from './abis'
|
||||
import { ethers } from 'ethers'
|
||||
import { getUrl } from './platform'
|
||||
import { generateApiToken } from './warpcast-auth'
|
||||
|
||||
const WARPCAST_BASE = 'https://client.warpcast.com/v2/'
|
||||
const EP_SIGNIN = `${WARPCAST_BASE}sign-in-with-farcaster`
|
||||
const FC_ID_REGISTRY_CONTRACT = '0x00000000fc6c5f01fc30151999387bb99a9f489b'
|
||||
|
||||
export const extractLinkData = (link: string) => {
|
||||
const url = new URL(link);
|
||||
const channelToken = url.searchParams.get('channelToken');
|
||||
const nonce = url.searchParams.get('nonce');
|
||||
const siweUri = url.searchParams.get('siweUri');
|
||||
const domain = url.searchParams.get('domain');
|
||||
const notBefore = url.searchParams.get('notBefore');
|
||||
const expirationTime = url.searchParams.get('expirationTime');
|
||||
|
||||
return {
|
||||
channelToken,
|
||||
nonce,
|
||||
siweUri,
|
||||
domain,
|
||||
notBefore,
|
||||
expirationTime,
|
||||
} as {
|
||||
channelToken: string,
|
||||
nonce: string,
|
||||
siweUri: string,
|
||||
domain: string,
|
||||
notBefore: string,
|
||||
expirationTime: string,
|
||||
}
|
||||
}
|
||||
|
||||
export const validateLinkData = (link: string) => {
|
||||
const { channelToken, nonce, siweUri, domain, notBefore, expirationTime } = extractLinkData(link);
|
||||
if (!channelToken || !nonce || !siweUri || !domain || !notBefore || !expirationTime) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export const constructWarpcastSWIEMsg = ({
|
||||
siweUri,
|
||||
domain,
|
||||
nonce,
|
||||
notBefore,
|
||||
expirationTime,
|
||||
fid,
|
||||
custodyAddress
|
||||
}: {
|
||||
siweUri: string,
|
||||
domain: string,
|
||||
nonce: string,
|
||||
notBefore: string,
|
||||
expirationTime: string,
|
||||
fid: number,
|
||||
custodyAddress: string
|
||||
}) => {
|
||||
return `${domain} wants you to sign in with your Ethereum account:\n${custodyAddress}\n\nFarcaster Auth\n\nURI: ${siweUri}\nVersion: 1\nChain ID: 10\nNonce: ${nonce}\nIssued At: ${notBefore}\nExpiration Time: ${expirationTime}\nNot Before: ${notBefore}\nResources:\n- farcaster://fid/${fid}`
|
||||
}
|
||||
|
||||
|
||||
export const signInWithFarcaster = async ({
|
||||
channelToken,
|
||||
message,
|
||||
signature,
|
||||
authToken
|
||||
} : {
|
||||
channelToken: string,
|
||||
message: string,
|
||||
signature: string,
|
||||
authToken: string
|
||||
}) => {
|
||||
const response = await fetch(`${EP_SIGNIN}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
channelToken,
|
||||
message,
|
||||
signature,
|
||||
})
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const noFidNotification = () => {
|
||||
const messageId = Math.floor(Math.random() * 1000000);
|
||||
|
||||
chrome.notifications.create('no-fid', {
|
||||
type: 'basic',
|
||||
iconUrl: getUrl('assets/extension-icon/wallet_128.png'),
|
||||
title: 'Error',
|
||||
message: 'This addres does not own any FID please select custody address that owns your FID.\nMessage ID: ' + messageId
|
||||
});
|
||||
}
|
||||
|
||||
export const getFidFromAddress = async (address: string) : Promise<number | null> => {
|
||||
const provider = await getOptimismProvider();
|
||||
const contract = new ethers.Contract(FC_ID_REGISTRY_CONTRACT, FARCASTER_PARTIAL_KEY_ABI, provider);
|
||||
const FID = await contract.idOf(address);
|
||||
if (FID > 0) {
|
||||
return FID;
|
||||
}
|
||||
noFidNotification();
|
||||
return 0;
|
||||
}
|
||||
|
||||
export const doSignInWithFarcaster = async ({
|
||||
link
|
||||
}: {
|
||||
link: string
|
||||
}) => {
|
||||
const { channelToken, nonce, siweUri, domain, notBefore, expirationTime } = extractLinkData(link);
|
||||
const custodyAddress = (await getSelectedAddress())?.[0] || '';
|
||||
const fid = custodyAddress && await getFidFromAddress(custodyAddress);
|
||||
if (!fid) {
|
||||
return -1;
|
||||
}
|
||||
const message = constructWarpcastSWIEMsg({
|
||||
siweUri,
|
||||
domain,
|
||||
nonce,
|
||||
notBefore,
|
||||
expirationTime,
|
||||
fid,
|
||||
custodyAddress
|
||||
});
|
||||
|
||||
const genToken = await generateApiToken();
|
||||
let authToken = '';
|
||||
if(genToken.success) {
|
||||
authToken = genToken.data;
|
||||
}
|
||||
|
||||
console.log('authToken', authToken);
|
||||
|
||||
if (!authToken) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
const signature = await signMsg(message);
|
||||
await signInWithFarcaster({
|
||||
channelToken,
|
||||
message,
|
||||
signature,
|
||||
authToken
|
||||
});
|
||||
|
||||
return 1
|
||||
}
|
||||
|
@ -315,4 +315,4 @@ export const openTab = (url: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
export const getVersion = () => chrome?.runtime?.getManifest()?.version ?? ''
|
||||
export const getVersion = () => chrome?.runtime?.getManifest()?.version ?? ''
|
@ -1,5 +1,6 @@
|
||||
import { getSelectedAccount, getSelectedNetwork, numToHexStr } from '@/utils/platform';
|
||||
import { ethers } from "ethers"
|
||||
import { mainNets } from '@/utils/networks';
|
||||
|
||||
let provider: ethers.JsonRpcProvider | null = null
|
||||
|
||||
@ -16,6 +17,11 @@ export const getCurrentProvider = async () => {
|
||||
return {provider, network}
|
||||
}
|
||||
|
||||
export const getOptimismProvider = async () => {
|
||||
const network = mainNets[10]
|
||||
return new ethers.JsonRpcProvider(network.rpc, ethers.Network.from(network.chainId), { staticNetwork: true, batchMaxCount: 6, polling: false })
|
||||
}
|
||||
|
||||
const convertReceipt = (receipt: ethers.TransactionReceipt | null) => {
|
||||
if(!receipt) return null
|
||||
const newReceipt = {...receipt} as any
|
||||
|
161
src/utils/warpcast-auth.ts
Normal file
161
src/utils/warpcast-auth.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { signMsg } from './wallet'
|
||||
import { getBytes } from 'ethers';
|
||||
import bufferLib from 'buffer';
|
||||
|
||||
const EIP_191_PREFIX = "eip191:";
|
||||
const WARPCAST_API = 'https://client.warpcast.com/v2'
|
||||
|
||||
const NO_WALLET = 'NO_WALLET'
|
||||
const SIG_DENIED = 'SIG_DENIED'
|
||||
const NO_AUTH_TOKEN = 'NO_AUTH_TOKEN'
|
||||
const AUTH_SUCCESS = 'AUTH_SUCCESS'
|
||||
|
||||
type T_RESULT_GEN_AUTH_TOKEN = {
|
||||
success: boolean;
|
||||
data: typeof SIG_DENIED | typeof NO_AUTH_TOKEN | typeof AUTH_SUCCESS | typeof NO_WALLET | string;
|
||||
}
|
||||
|
||||
type T_IDDB_VALUE = {
|
||||
secret: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
function serialize (object: any) {
|
||||
if (typeof object === 'number' && isNaN(object)) {
|
||||
throw new Error('NaN is not allowed');
|
||||
}
|
||||
|
||||
if (typeof object === 'number' && !isFinite(object)) {
|
||||
throw new Error('Infinity is not allowed');
|
||||
}
|
||||
|
||||
if (object === null || typeof object !== 'object') {
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
|
||||
if (object.toJSON instanceof Function) {
|
||||
return serialize(object.toJSON());
|
||||
}
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
const values: any = object.reduce((t, cv, ci) => {
|
||||
const comma = ci === 0 ? '' : ',';
|
||||
const value = cv === undefined || typeof cv === 'symbol' ? null : cv;
|
||||
return `${t}${comma}${serialize(value)}`;
|
||||
}, '');
|
||||
return `[${values}]`;
|
||||
}
|
||||
|
||||
const values: any = Object.keys(object).sort().reduce((t, cv) => {
|
||||
if (object[cv] === undefined ||
|
||||
typeof object[cv] === 'symbol') {
|
||||
return t;
|
||||
}
|
||||
const comma = t.length === 0 ? '' : ',';
|
||||
return `${t}${comma}${serialize(cv)}:${serialize(object[cv])}`;
|
||||
}, '');
|
||||
return `{${values}}`;
|
||||
};
|
||||
|
||||
function createWarpMessage (data: any) {
|
||||
return { message: serialize(data) }
|
||||
}
|
||||
|
||||
|
||||
export const generateApiToken = async (): Promise<T_RESULT_GEN_AUTH_TOKEN> => {
|
||||
try {
|
||||
|
||||
const timestamp = Date.now();
|
||||
const payload = {
|
||||
method: "generateToken",
|
||||
params: {
|
||||
timestamp,
|
||||
expiresAt: 1777046287381
|
||||
},
|
||||
};
|
||||
const msgToSign = createWarpMessage(payload);
|
||||
|
||||
|
||||
const sig = await signMsg(msgToSign.message);
|
||||
|
||||
const Buffer = bufferLib.Buffer;
|
||||
|
||||
const sigBase64 = Buffer.from(getBytes(sig)).toString('base64');
|
||||
const cusAuth = EIP_191_PREFIX + sigBase64
|
||||
|
||||
const req = await fetch(`${WARPCAST_API}/auth`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${cusAuth}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
|
||||
if (req.ok) {
|
||||
const data = await req.json();
|
||||
const token = data?.result?.token?.secret;
|
||||
if (token) {
|
||||
return { success: true, data: token }
|
||||
}
|
||||
return { success: false, data: NO_AUTH_TOKEN }
|
||||
}
|
||||
return { success: false, data: NO_AUTH_TOKEN }
|
||||
} catch (error) {
|
||||
console.error('Failed to generate api token', error)
|
||||
return { success: false, data: NO_AUTH_TOKEN}
|
||||
}
|
||||
}
|
||||
|
||||
export const addWarpAuthToken = async (value: T_IDDB_VALUE): Promise<unknown> => {
|
||||
const dbName = 'localforage'
|
||||
const storeName = 'keyvaluepairs'
|
||||
const key = 'auth-token'
|
||||
const version = 2
|
||||
let resolve = (a = false) => {}
|
||||
const result = new Promise((res) => {
|
||||
resolve = res
|
||||
})
|
||||
|
||||
try {
|
||||
const dbRequest = indexedDB.open(dbName, version);
|
||||
|
||||
dbRequest.onupgradeneeded = (event: any) => {
|
||||
const db = event.target.result;
|
||||
db.createObjectStore(storeName);
|
||||
};
|
||||
|
||||
dbRequest.onsuccess = (event: any) => {
|
||||
|
||||
const db = dbRequest.result
|
||||
const transaction = db.transaction(storeName, "readwrite");
|
||||
const store = transaction.objectStore(storeName);
|
||||
|
||||
const request = store.put(value, key);
|
||||
|
||||
request.onsuccess = (event: any) => {
|
||||
console.log("Successfully added data:", event.target.result);
|
||||
window?.location?.reload()
|
||||
resolve?.()
|
||||
}
|
||||
|
||||
request.onerror = (event: any) => {
|
||||
console.error("Error adding data:", event.target.error);
|
||||
resolve?.()
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
dbRequest.onerror = (event: any) => {
|
||||
console.error("Error adding data:", event.target.error);
|
||||
resolve?.()
|
||||
};
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error accessing IndexedDB:", error);
|
||||
resolve?.()
|
||||
}
|
||||
return result
|
||||
}
|
468
src/views/FarcasterActions.vue
Normal file
468
src/views/FarcasterActions.vue
Normal file
@ -0,0 +1,468 @@
|
||||
<template>
|
||||
<ion-page>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button @click="onCancel">Back</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Farcaster Actions</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item>
|
||||
<ion-label style="opacity: 0.9; font-size: 0.85rem"
|
||||
>Selected address needs to own a FID in order to work, this address is also
|
||||
known as custody address.
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label style="opacity: 0.9; font-size: 0.85rem"
|
||||
>These are experimental features RE from Warpcast might not work in all cases
|
||||
and might break if WC makes changes.</ion-label
|
||||
>
|
||||
</ion-item>
|
||||
<div
|
||||
style="border: 1px solid var(--ion-color-medium-contrast);
|
||||
margin: 0.6rem;
|
||||
}"
|
||||
>
|
||||
<ion-item>
|
||||
<ion-label>Selected Account: {{ selectedAccount?.name }}</ion-label>
|
||||
<ion-button
|
||||
@click="
|
||||
() => {
|
||||
accountsModal = true;
|
||||
toastState = false;
|
||||
}
|
||||
"
|
||||
>Select</ion-button
|
||||
>
|
||||
</ion-item>
|
||||
<ion-item button>
|
||||
<p style="font-size: 0.7rem; color: coral">{{ selectedAccount?.address }}</p>
|
||||
</ion-item>
|
||||
</div>
|
||||
<ion-item>
|
||||
<ion-label style="opacity: 0.9; font-size: 0.85rem"
|
||||
>Used for sign in with farcaster/warpcast QR you'll need to paste the deep link
|
||||
in next screen</ion-label
|
||||
></ion-item
|
||||
>
|
||||
<ion-item>
|
||||
<ion-button
|
||||
@click="swiwModal = true"
|
||||
color="light"
|
||||
style="
|
||||
margin: auto;
|
||||
transform: scale(1.2);
|
||||
filter: hue-rotate(59deg) saturate(1.5) sepia(0.1);
|
||||
"
|
||||
>Sign in with farcaster</ion-button
|
||||
>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label style="opacity: 0.9; font-size: 0.85rem"
|
||||
>Used to login on warpcast.com without needing a mobile device</ion-label
|
||||
>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-button
|
||||
@click="promptForSignIn"
|
||||
style="
|
||||
margin: auto;
|
||||
transform: scale(1.2);
|
||||
filter: hue-rotate(59deg) saturate(1.5) sepia(0.1);
|
||||
"
|
||||
color="light"
|
||||
>Login on Warpcast.com</ion-button
|
||||
>
|
||||
</ion-item>
|
||||
|
||||
<ion-alert
|
||||
:is-open="alertOpen"
|
||||
header="Error"
|
||||
:message="alertMsg"
|
||||
:buttons="['OK']"
|
||||
@didDismiss="alertOpen = false"
|
||||
></ion-alert>
|
||||
|
||||
<ion-modal :is-open="accountsModal">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button @click="accountsModal = false">Close</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Select</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<ion-list style="margin-bottom: 4rem">
|
||||
<ion-radio-group :value="selectedAccount?.address ?? ''">
|
||||
<ion-list-header>
|
||||
<ion-label>Accounts</ion-label>
|
||||
</ion-list-header>
|
||||
|
||||
<ion-list
|
||||
@click="changeSelectedAccount(account.address)"
|
||||
class="ion-padding"
|
||||
v-for="account of accounts"
|
||||
:key="account.address"
|
||||
button
|
||||
>
|
||||
<ion-item>
|
||||
<ion-radio
|
||||
:aria-label="account.name"
|
||||
slot="start"
|
||||
:value="account.address"
|
||||
>{{ account.name }}</ion-radio
|
||||
>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-text style="font-size: 0.7rem; color: coral">{{
|
||||
account.address
|
||||
}}</ion-text>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
|
||||
<ion-modal :is-open="swiwModal" @didDismiss="deepLink = ''">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button @click="swiwModal = false">Close</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Paste Link To Authorize</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item>
|
||||
<ion-label
|
||||
>Enter deep-link from Sign in with farcaster QR EX:
|
||||
<span style="font-size: 0.8rem; opacity: 0.8">
|
||||
https://warpcast.com/~/sign-in-with-farcaster?channelToken=4a8d3f27-....
|
||||
</span></ion-label
|
||||
>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-textarea
|
||||
style="overflow-y: scroll; width: 100%"
|
||||
aria-label="Enter deep link from Sign in with farcaste QR"
|
||||
:rows="10"
|
||||
:cols="10"
|
||||
v-model="deepLink"
|
||||
></ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-button @click="swiwModal = false" color="light">Cancel</ion-button>
|
||||
<ion-button @click="farcasterSWIWAithorize">Authorize</ion-button>
|
||||
</ion-item>
|
||||
</ion-content>
|
||||
|
||||
<ion-loading
|
||||
:is-open="swloading"
|
||||
cssClass="my-custom-class"
|
||||
message="Please wait..."
|
||||
:duration="4000"
|
||||
:key="`k${swloading}`"
|
||||
@didDismiss="swloading = false"
|
||||
>
|
||||
</ion-loading>
|
||||
|
||||
<ion-loading
|
||||
:is-open="warpcastLoading"
|
||||
cssClass="my-custom-class"
|
||||
message="Please wait..."
|
||||
:duration="4000"
|
||||
:key="`k${warpcastLoading}`"
|
||||
@didDismiss="warpcastLoading = false"
|
||||
>
|
||||
</ion-loading>
|
||||
</ion-modal>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, Ref } from "vue";
|
||||
import {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonInput,
|
||||
IonButton,
|
||||
IonAlert,
|
||||
IonIcon,
|
||||
onIonViewWillEnter,
|
||||
modalController,
|
||||
IonModal,
|
||||
IonButtons,
|
||||
IonTextarea,
|
||||
} from "@ionic/vue";
|
||||
import { saveSelectedAccount, paste, replaceAccounts } from "@/utils/platform";
|
||||
import router from "@/router";
|
||||
import type { Account } from "@/extension/types";
|
||||
import UnlockModal from "@/views/UnlockModal.vue";
|
||||
import { triggerListner } from "@/extension/listners";
|
||||
import { copyOutline } from "ionicons/icons";
|
||||
|
||||
import { clipboardOutline } from "ionicons/icons";
|
||||
import {
|
||||
doSignInWithFarcaster,
|
||||
validateLinkData,
|
||||
getFidFromAddress,
|
||||
} from "@/utils/farcaster";
|
||||
import { getAccounts, getSelectedAccount, unBlockLockout } from "@/utils/platform";
|
||||
import { addWarpAuthToken, generateApiToken } from "@/utils/warpcast-auth";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonInput,
|
||||
IonButton,
|
||||
IonAlert,
|
||||
IonIcon,
|
||||
IonModal,
|
||||
IonButtons,
|
||||
IonTextarea,
|
||||
},
|
||||
setup: () => {
|
||||
const name = ref("");
|
||||
const pk = ref("");
|
||||
const alertOpen = ref(false);
|
||||
const alertMsg = ref("");
|
||||
const swiwModal = ref(false);
|
||||
const deepLink = ref("");
|
||||
const swloading = ref(false);
|
||||
const warpcastLoading = ref(false);
|
||||
|
||||
const loading = ref(false);
|
||||
const accounts = ref([]) as Ref<Account[]>;
|
||||
const accountsModal = ref(false) as Ref<boolean>;
|
||||
const selectedAccount = (ref(null) as unknown) as Ref<Account>;
|
||||
const toastState = ref(false);
|
||||
|
||||
const loadData = () => {
|
||||
loading.value = true;
|
||||
const pAccounts = getAccounts();
|
||||
const pSelectedAccount = getSelectedAccount();
|
||||
Promise.all([pAccounts, pSelectedAccount]).then((res) => {
|
||||
accounts.value = res[0];
|
||||
selectedAccount.value = res[1];
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
onIonViewWillEnter(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
const onCancel = () => {
|
||||
router.push("/tabs/home");
|
||||
};
|
||||
|
||||
const changeSelectedAccount = async (address: string) => {
|
||||
loading.value = true;
|
||||
const findIndex = accounts.value.findIndex((a) => a.address == address);
|
||||
if (findIndex > -1) {
|
||||
selectedAccount.value = accounts.value[findIndex];
|
||||
accounts.value = accounts.value.filter((a) => a.address !== address);
|
||||
accounts.value.unshift(selectedAccount.value);
|
||||
const newAccounts = [...accounts.value];
|
||||
await Promise.all([
|
||||
saveSelectedAccount(selectedAccount.value),
|
||||
replaceAccounts(newAccounts),
|
||||
]);
|
||||
triggerListner("accountsChanged", [newAccounts.map((a) => a.address)?.[0]]);
|
||||
}
|
||||
accountsModal.value = false;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const farcasterSWIWAithorize = async () => {
|
||||
if (!deepLink.value) {
|
||||
alertMsg.value = "Please enter the deep link";
|
||||
alertOpen.value = true;
|
||||
return;
|
||||
}
|
||||
const linkData = validateLinkData(deepLink.value);
|
||||
if (!linkData) {
|
||||
alertMsg.value = "Invalid deep link";
|
||||
alertOpen.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((selectedAccount.value.pk ?? "").length !== 66) {
|
||||
const modalResult = await openModal();
|
||||
if (modalResult) {
|
||||
unBlockLockout();
|
||||
loading.value = true;
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
} else {
|
||||
unBlockLockout();
|
||||
}
|
||||
swloading.value = true;
|
||||
try {
|
||||
const result = await doSignInWithFarcaster({
|
||||
link: deepLink.value,
|
||||
});
|
||||
|
||||
if (result === -1) {
|
||||
alertMsg.value =
|
||||
"Selected account does not own a FID please select an account that owns a FID";
|
||||
alertOpen.value = true;
|
||||
swloading.value = false;
|
||||
return;
|
||||
} else if (result === -2) {
|
||||
alertMsg.value = "Optimism RCP is not available";
|
||||
alertOpen.value = true;
|
||||
swloading.value = false;
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
alertMsg.value = String(e);
|
||||
alertOpen.value = true;
|
||||
}
|
||||
swloading.value = false;
|
||||
router.push("/tabs/home");
|
||||
};
|
||||
|
||||
const promptForSignIn = async () => {
|
||||
const targetUrl = "warpcast.com";
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) {
|
||||
const lastTab = tabs[0];
|
||||
|
||||
if (!lastTab) {
|
||||
alertMsg.value = "No active tab found";
|
||||
alertOpen.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastTab?.url?.includes(targetUrl)) {
|
||||
alertOpen.value = true;
|
||||
alertMsg.value = "You are not on warpcast.com page";
|
||||
|
||||
return;
|
||||
}
|
||||
if (!lastTab.id) {
|
||||
alertMsg.value = "No active tab found";
|
||||
alertOpen.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((selectedAccount.value.pk ?? "").length !== 66) {
|
||||
const modalResult = await openModal();
|
||||
if (modalResult) {
|
||||
unBlockLockout();
|
||||
loading.value = true;
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
} else {
|
||||
unBlockLockout();
|
||||
}
|
||||
|
||||
warpcastLoading.value = true;
|
||||
|
||||
let hasFid = 0 as number | null;
|
||||
|
||||
try {
|
||||
hasFid = await getFidFromAddress(selectedAccount.value.address);
|
||||
} catch (e) {
|
||||
alertMsg.value = String(e);
|
||||
alertOpen.value = true;
|
||||
warpcastLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasFid) {
|
||||
alertMsg.value =
|
||||
"Selected account does not own a FID please select an account that owns a FID";
|
||||
alertOpen.value = true;
|
||||
warpcastLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let token = "";
|
||||
|
||||
try {
|
||||
const data = await generateApiToken();
|
||||
if (data.success) {
|
||||
token = data.data;
|
||||
} else {
|
||||
alertMsg.value = `Error in generating Auth token: ${data.data}`;
|
||||
alertOpen.value = true;
|
||||
warpcastLoading.value = false;
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
alertMsg.value = String(e);
|
||||
alertOpen.value = true;
|
||||
warpcastLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const arg = { secret: token, expiresAt: 1777046287381 };
|
||||
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId: lastTab.id },
|
||||
func: addWarpAuthToken,
|
||||
args: [arg],
|
||||
});
|
||||
|
||||
window.close();
|
||||
});
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const modal = await modalController.create({
|
||||
component: UnlockModal,
|
||||
componentProps: {
|
||||
unlockType: "transaction",
|
||||
},
|
||||
});
|
||||
modal.present();
|
||||
const { role } = await modal.onWillDismiss();
|
||||
if (role === "confirm") return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
name,
|
||||
pk,
|
||||
onCancel,
|
||||
alertOpen,
|
||||
alertMsg,
|
||||
clipboardOutline,
|
||||
paste,
|
||||
accountsModal,
|
||||
changeSelectedAccount,
|
||||
selectedAccount,
|
||||
accounts,
|
||||
copyOutline,
|
||||
toastState,
|
||||
deepLink,
|
||||
swiwModal,
|
||||
farcasterSWIWAithorize,
|
||||
swloading,
|
||||
promptForSignIn,
|
||||
warpcastLoading,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
@ -137,6 +137,11 @@
|
||||
</p>
|
||||
</div>
|
||||
</ion-item>
|
||||
<ion-item style="margin-top: 0.3rem; margin-bottom: 0.3rem; text-align: center">
|
||||
<ion-button @click="goToFarcasterActions" expand="block"
|
||||
>Experimental Farcaster Wallet Actions</ion-button
|
||||
>
|
||||
</ion-item>
|
||||
|
||||
<ion-loading
|
||||
:is-open="loading"
|
||||
@ -368,6 +373,10 @@ export default defineComponent({
|
||||
router.push("/tabs/add-network");
|
||||
};
|
||||
|
||||
const goToFarcasterActions = () => {
|
||||
router.push("/farcaster-actions");
|
||||
};
|
||||
|
||||
const changeSelectedAccount = async (address: string) => {
|
||||
loading.value = true;
|
||||
const findIndex = accounts.value.findIndex((a) => a.address == address);
|
||||
@ -421,6 +430,7 @@ export default defineComponent({
|
||||
openTab,
|
||||
settings,
|
||||
version,
|
||||
goToFarcasterActions,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -143,6 +143,9 @@ export default defineComponent({
|
||||
|
||||
const onSign = async () => {
|
||||
loading.value = true;
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
const selectedAccount = await getSelectedAccount();
|
||||
loading.value = false;
|
||||
if ((selectedAccount.pk ?? "").length !== 66) {
|
||||
|
@ -270,6 +270,29 @@ export default defineComponent({
|
||||
signTxData.value = JSON.stringify(paramsWithoutZeros, null, 2);
|
||||
}
|
||||
|
||||
const setItervalFn = async () => {
|
||||
if (timerReject.value <= 0) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
if (gasPriceReFetch.value) {
|
||||
timerFee.value -= 1;
|
||||
if (timerFee.value <= 0) {
|
||||
timerFee.value = 20;
|
||||
loading.value = true;
|
||||
const { feed, price } = await getGasPrice();
|
||||
gasFeed = feed;
|
||||
gasPrice.value = parseFloat(price.toString() ?? 0.1);
|
||||
await newGasData();
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
timerReject.value -= 1;
|
||||
bars.value++;
|
||||
walletPing();
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const modal = await modalController.create({
|
||||
component: UnlockModal,
|
||||
@ -285,6 +308,9 @@ export default defineComponent({
|
||||
|
||||
const onSign = async () => {
|
||||
loading.value = true;
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
const selectedAccount = await getSelectedAccount();
|
||||
loading.value = false;
|
||||
if ((selectedAccount.pk ?? "").length !== 66) {
|
||||
@ -370,28 +396,7 @@ export default defineComponent({
|
||||
await newGasData();
|
||||
loading.value = false;
|
||||
|
||||
interval = setInterval(async () => {
|
||||
if (timerReject.value <= 0) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
if (gasPriceReFetch.value) {
|
||||
timerFee.value -= 1;
|
||||
if (timerFee.value <= 0) {
|
||||
timerFee.value = 20;
|
||||
loading.value = true;
|
||||
const { feed, price } = await getGasPrice();
|
||||
gasFeed = feed;
|
||||
gasPrice.value = parseFloat(price.toString() ?? 0.1);
|
||||
await newGasData();
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
timerReject.value -= 1;
|
||||
bars.value++;
|
||||
walletPing();
|
||||
}, 1000) as any;
|
||||
interval = setInterval(setItervalFn, 1000) as any;
|
||||
});
|
||||
|
||||
const setGasLimit = () => {
|
||||
|
@ -154,14 +154,29 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
if (passInput.value) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
console.log("passInput.value", passInput.value);
|
||||
onMounted(async () => {
|
||||
console.log("rendered");
|
||||
if (passInput.value) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
passInput.value.$el.setFocus();
|
||||
passInput.value.$el.addEventListener("keyup", (e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
unlock();
|
||||
}
|
||||
});
|
||||
passInput.value.$el.addEventListener("blur", () => {
|
||||
passInput.value.$el.setFocus();
|
||||
}
|
||||
});
|
||||
passInput.value.$el.selectionStart = passInput.value?.$el.value.length;
|
||||
});
|
||||
}
|
||||
|
||||
// requestAnimationFrame(async () => {
|
||||
// if (passInput.value) {
|
||||
// await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
// console.log("passInput.value", passInput.value);
|
||||
// passInput.value.$el.setFocus();
|
||||
// }
|
||||
// });
|
||||
});
|
||||
|
||||
return {
|
||||
|
Loading…
Reference in New Issue
Block a user