diff --git a/CHANGELOG.md b/CHANGELOG.md index 739fa36..a595e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Manifest Version 1.4.4 + +- added QR scaner for easier sign in with farcaster +- added cyber network in the network templates +- style changes and changes to fracaster action screen + ## Manifest Version 1.4.3 - changed sign in with farcaster to work with new type of QR code diff --git a/package.json b/package.json index 42d672b..8af239e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@ionic/vue-router": "^8.2.6", "core-js": "^3.38.0", "ethers": "^6.13.2", + "qr-scanner": "^1.4.2", "vue": "^3.4.37", "vue-router": "^4.4.3" }, diff --git a/src/utils/QR.ts b/src/utils/QR.ts new file mode 100644 index 0000000..2c1704f --- /dev/null +++ b/src/utils/QR.ts @@ -0,0 +1,26 @@ +import QrScanner from 'qr-scanner'; + +function base64ImageToBlob(str: string) { + const pos = str.indexOf(";base64,"); + const type = str.substring(5, pos); + const b64 = str.substr(pos + 8); + const imageContent = atob(b64); + const buffer = new ArrayBuffer(imageContent.length); + const view = new Uint8Array(buffer); + for(var n = 0; n < imageContent.length; n++) { + view[n] = imageContent.charCodeAt(n); + } + const blob = new Blob([buffer], { type: type }); + return blob; + } + +export async function getQRCode(imageBase64: string) { + try { + const imageBlob = base64ImageToBlob(imageBase64); + + return await QrScanner.scanImage(imageBlob) + } catch (e) { + console.error(e); + return null; + } + } diff --git a/src/utils/farcaster.ts b/src/utils/farcaster.ts index e45e1cc..7d57031 100644 --- a/src/utils/farcaster.ts +++ b/src/utils/farcaster.ts @@ -3,20 +3,21 @@ import { FARCASTER_PARTIAL_KEY_ABI } from './abis' import { ethers } from 'ethers' import { getUrl } from './platform' import { generateApiToken } from './warpcast-auth' +import { getQRCode } from './QR' export interface TChannelTokenStatusResponse { - state: string; - nonce: string; + state: string; + nonce: string; signatureParams: { - siweUri: string; - domain: string; - nonce: string; - notBefore: string; + siweUri: string; + domain: string; + nonce: string; + notBefore: string; expirationTime: string; } ; - metadata: { + metadata: { userAgent: string; - ip: string; + ip: string; }; } @@ -25,13 +26,17 @@ 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 getLinkFromQR = async () => { + const data = await chrome.tabs.captureVisibleTab() + return await getQRCode(data) +} const getChannelTokenStatus = async (channelToken: string) => { const response = await fetch(`${TOKEN_STATUS_ENDPOINT}`, { method: 'GET', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${channelToken}` + 'Authorization': `Bearer ${channelToken}` }, }) return response.json() as Promise @@ -65,23 +70,23 @@ export const extractLinkData = (link: string) => { export const extractResponseData = async (channelToken: string) => { try { - const response = await getChannelTokenStatus(channelToken); - let { siweUri, domain, nonce, notBefore, expirationTime } = response.signatureParams; - nonce = nonce || (Math.random() + 1).toString(36).substring(7); - return { - siweUri, - domain, - nonce, - notBefore, - expirationTime - } - } catch (e) { - return null; + const response = await getChannelTokenStatus(channelToken); + let { siweUri, domain, nonce, notBefore, expirationTime } = response.signatureParams; + nonce = nonce || (Math.random() + 1).toString(36).substring(7); + return { + siweUri, + domain, + nonce, + notBefore, + expirationTime + } + } catch (e) { + return null; } } export const validateLinkData = (link: string) => { - const { channelToken} = extractLinkData(link); + const { channelToken } = extractLinkData(link); if (!channelToken) { return false; } @@ -106,16 +111,16 @@ export const constructWarpcastSWIEMsg = ({ 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}${notBefore ? `\nIssued At: ${notBefore}`: `\nIssued At: ${new Date(Date.now() - 1000).toISOString()}`}${expirationTime ? `\nExpiration Time: ${expirationTime}` : ''}${notBefore ? `\nNot Before: ${notBefore}`: ''}\nResources:\n- farcaster://fid/${fid}` + 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}${notBefore ? `\nIssued At: ${notBefore}` : `\nIssued At: ${new Date(Date.now() - 1000).toISOString()}`}${expirationTime ? `\nExpiration Time: ${expirationTime}` : ''}${notBefore ? `\nNot Before: ${notBefore}` : ''}\nResources:\n- farcaster://fid/${fid}` } - + export const signInWithFarcaster = async ({ channelToken, message, signature, authToken -} : { +}: { channelToken: string, message: string, signature: string, @@ -125,7 +130,7 @@ export const signInWithFarcaster = async ({ method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${authToken}` + 'Authorization': `Bearer ${authToken}` }, body: JSON.stringify({ channelToken, @@ -148,9 +153,9 @@ const noFidNotification = () => { }); } -export const getFidFromAddress = async (address: string) : Promise => { +export const getFidFromAddress = async (address: string): Promise => { const provider = await getOptimismProvider(); - const contract = new ethers.Contract(FC_ID_REGISTRY_CONTRACT, FARCASTER_PARTIAL_KEY_ABI, provider); + 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; @@ -159,6 +164,66 @@ export const getFidFromAddress = async (address: string) : Promise { + const custodyAddress = (await getSelectedAddress())?.[0] || ''; + const fid = custodyAddress && await getFidFromAddress(custodyAddress); + if (!fid) { + return -1; + } + + const link = await getLinkFromQR(); + if (!link) { + return -2; + } + + const validateLinkDataResult = validateLinkData(link); + if (!validateLinkDataResult) { + return -3; + } + + const { channelToken } = extractLinkData(link); + + const extractResult = await extractResponseData(channelToken); + + if (!extractResult) { + return -4; + } + + const { siweUri, domain, nonce, notBefore, expirationTime } = extractResult + + const message = constructWarpcastSWIEMsg({ + siweUri, + domain, + nonce, + notBefore, + expirationTime, + fid, + custodyAddress + }); + + const genToken = await generateApiToken(); + + let authToken = ''; + if (genToken.success) { + authToken = genToken.data; + } + + if (!authToken) { + return -5; + } + + const signature = await signMsg(message); + await signInWithFarcaster({ + channelToken, + message, + signature, + authToken + }); + + return 1 + +} + export const doSignInWithFarcaster = async ({ link }: { @@ -166,7 +231,7 @@ export const doSignInWithFarcaster = async ({ }) => { const { channelToken } = extractLinkData(link); const custodyAddress = (await getSelectedAddress())?.[0] || ''; - const fid = custodyAddress && await getFidFromAddress(custodyAddress); + const fid = custodyAddress && await getFidFromAddress(custodyAddress); if (!fid) { return -1; } @@ -191,7 +256,7 @@ export const doSignInWithFarcaster = async ({ const genToken = await generateApiToken(); let authToken = ''; - if(genToken.success) { + if (genToken.success) { authToken = genToken.data; } @@ -209,4 +274,3 @@ export const doSignInWithFarcaster = async ({ return 1 } - diff --git a/src/views/FarcasterActions.vue b/src/views/FarcasterActions.vue index d2b2406..45e3058 100644 --- a/src/views/FarcasterActions.vue +++ b/src/views/FarcasterActions.vue @@ -18,8 +18,8 @@ These are experimental features RE from Warpcast might not work in all cases - and might break if WC makes changes.These are experimental features might not work in all cases and might break + when warpcast/farcaster make changes.
Used for sign in with farcaster/warpcast QR you'll need to paste the deep link - in next screenUsed for sign in with farcaster/warpcast QR, by scanning QR on pasteing + link. - Sign in with farcaster + - Login on Warpcast.com + Close - Paste Link To Authorize + Authorize Enter deep-link from Sign in with farcaster QR EX: - - https://warpcast.com/~/sign-in-with-farcaster?channelToken=4a8d3f27-.... -

Try to scan QR

+

+ (must be visible on current page) +

+ + Cancel + Authorize using QR + + + +

Alternative: paste link from QR

+

+ similar to: https://warpcast.com/~/siwf?channelToken=AXLUD4S4 +

+
+ + + +

Account needs to own a fid

+

QR needs to be visible on the website you click authorize

+
+
Cancel - Authorize + Authorize using link
@@ -224,6 +226,7 @@ import { doSignInWithFarcaster, validateLinkData, getFidFromAddress, + doSignInWithFarcasterQR, } from "@/utils/farcaster"; import { getAccounts, getSelectedAccount, unBlockLockout } from "@/utils/platform"; import { addWarpAuthToken, generateApiToken } from "@/utils/warpcast-auth"; @@ -300,7 +303,7 @@ export default defineComponent({ loading.value = false; }; - const farcasterSWIWAithorize = async () => { + const farcasterSWIWAuthorize = async () => { exitWallet.value = false; if (!deepLink.value) { alertMsg.value = "Please enter the deep link"; @@ -309,7 +312,7 @@ export default defineComponent({ } const linkData = validateLinkData(deepLink.value); if (!linkData) { - alertMsg.value = "Invalid deep link"; + alertMsg.value = "Invalid link pasted it does not contain a valid channel token"; alertOpen.value = true; return; } @@ -331,7 +334,54 @@ export default defineComponent({ link: deepLink.value, }); - console.log("result", result); + 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 = "Auth token generation failed"; + alertOpen.value = true; + swloading.value = false; + return; + } else if (result === -3) { + alertMsg.value = + "Error could read chanel token from data, make sure you have copied the correct link"; + alertOpen.value = true; + swloading.value = false; + return; + } else { + alertHeader.value = "OK"; + alertMsg.value = + "Request sent successfully, if QR is still open, you will be signed in"; + alertOpen.value = true; + swloading.value = false; + exitWallet.value = true; + } + } catch (e) { + alertMsg.value = String(e); + alertOpen.value = true; + } + swloading.value = false; + }; + + const farcasterSWIWQRAuthorize = async () => { + exitWallet.value = false; + 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 doSignInWithFarcasterQR(); if (result === -1) { alertMsg.value = @@ -340,20 +390,31 @@ export default defineComponent({ swloading.value = false; return; } else if (result === -2) { - alertMsg.value = "Optimism RCP is not available"; + alertMsg.value = + "Failed to read QR data, be sure QR is visible on, if is not working try using link"; alertOpen.value = true; swloading.value = false; return; } else if (result === -3) { alertMsg.value = - "Error could not get signer params from farcaster relay, try again"; + "QR does not contain a valid channel token, make sure you are scanning a sign in with farcaster QR"; + alertOpen.value = true; + swloading.value = false; + return; + } else if (result === -4) { + alertMsg.value = "Failed to extract sign params from QR"; + alertOpen.value = true; + swloading.value = false; + return; + } else if (result === -5) { + alertMsg.value = "Auth token generation failed"; alertOpen.value = true; swloading.value = false; return; } else { alertHeader.value = "OK"; alertMsg.value = - "Request sent successfully, if QR is still open, you should be signed in"; + "Request sent successfully, if QR is still open, you will be signed in"; alertOpen.value = true; swloading.value = false; exitWallet.value = true; @@ -481,14 +542,113 @@ export default defineComponent({ toastState, deepLink, swiwModal, - farcasterSWIWAithorize, + farcasterSWIWAuthorize, swloading, promptForSignIn, warpcastLoading, window, exitWallet, alertHeader, + farcasterSWIWQRAuthorize, }; }, }); + + diff --git a/yarn.lock b/yarn.lock index d937898..c052032 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1145,6 +1145,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/offscreencanvas@^2019.6.4": + version "2019.7.3" + resolved "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz#90267db13f64d6e9ccb5ae3eac92786a7c77a516" + integrity sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A== + "@types/readdir-glob@*": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.1.tgz#27ac2db283e6aa3d110b14ff9da44fcd1a5c38b1" @@ -4054,6 +4059,13 @@ q@^1.0.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== +qr-scanner@^1.4.2: + version "1.4.2" + resolved "https://registry.npmjs.org/qr-scanner/-/qr-scanner-1.4.2.tgz#bc4fb88022a8c9be95c49527a1c8fb8724b47dc4" + integrity sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw== + dependencies: + "@types/offscreencanvas" "^2019.6.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"