From 2dd756a0f1f2a126e1d43028654985827230f910 Mon Sep 17 00:00:00 2001 From: louis-md Date: Fri, 5 Jul 2024 13:05:18 +0200 Subject: [PATCH] Passkeys (#503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Passkeys in the SDK references (#495) * add passkeys in the doc references * Update pages/sdk/protocol-kit/reference/safe-factory.md Co-authored-by: Germán Martínez <6764315+germartinez@users.noreply.github.com> * Update pages/sdk/protocol-kit/reference/safe.md Co-authored-by: Germán Martínez <6764315+germartinez@users.noreply.github.com> * Update pages/sdk/protocol-kit/reference/safe.md Co-authored-by: Germán Martínez <6764315+germartinez@users.noreply.github.com> * Update pages/sdk/protocol-kit/reference/safe.md Co-authored-by: Germán Martínez <6764315+germartinez@users.noreply.github.com> * Update pages/sdk/relay-kit/reference/safe-4337-pack.mdx Co-authored-by: Germán Martínez <6764315+germartinez@users.noreply.github.com> --------- Co-authored-by: Germán Martínez <6764315+germartinez@users.noreply.github.com> * docs: Add Passkeys section, overview and guide (#498) * Add Passkeys section * Add Passkeys overview * Add passkeys guide * Create missing pages * Update steps style in Passkeys guide * fix: copy edits * feat: add safe and passkeys section * fix: vale errors * fix: formatting error * fix: updates based on feedback * feat: add flow diagram * fix: minor update * Add passkeys tutorial * Minor fixes to passkeys tutorial * Fix vale errors * Fix screenshots * Fix screenshots * fix: minor edits * fix: minor edits * Minor fixes * fix: correct term * Add passkeys FAQs * Minor fixes * feat: add new chains * Fix typo * Fix styling * fix: correct based on feedback * feat: add api reference for passkeys remove and swap owners (#505) * Fix typo in tutorial layout * Get tutorial code examples from github * Update 7579 code examples * Move passkeys code examples outside of the /pages folder * Add developer preview notice in 7579 and passkeys tutorials --------- Co-authored-by: Dani Somoza Co-authored-by: Germán Martínez <6764315+germartinez@users.noreply.github.com> Co-authored-by: Germán Martínez Co-authored-by: Tanay Pant Co-authored-by: leonardotc --- .github/scripts/generateCodeExamples.js | 62 +++++ .../config/vocabularies/default/accept.txt | 2 + assets/safe-passkeys-app-1.png | Bin 0 -> 50145 bytes assets/safe-passkeys-app-2.png | Bin 0 -> 90818 bytes examples/erc-7579/app/layout.tsx | 2 +- .../components/ScheduledTransferForm.tsx | 3 +- examples/erc-7579/lib/scheduledTransfers.ts | 21 +- examples/passkeys/app/layout.tsx | 83 +++++++ examples/passkeys/app/page.tsx | 149 +++++++++++ examples/passkeys/components/PasskeyList.tsx | 56 +++++ examples/passkeys/lib/constants.ts | 7 + examples/passkeys/lib/passkeys.ts | 137 ++++++++++ examples/passkeys/lib/usdc.ts | 89 +++++++ examples/passkeys/lib/utils.ts | 12 + package.json | 1 + .../erc-7579/tutorials/7579-tutorial.mdx | 4 + pages/advanced/smart-account-modules.mdx | 31 +-- .../4337-guides/permissionless-detailed.mdx | 4 + pages/home/_meta.json | 17 +- pages/home/passkeys-faqs.mdx | 26 ++ pages/home/passkeys-guides/_meta.json | 3 + pages/home/passkeys-guides/safe-sdk.mdx | 225 +++++++++++++++++ pages/home/passkeys-overview.mdx | 53 ++++ pages/home/passkeys-safe.mdx | 63 +++++ pages/home/passkeys-supported-networks.mdx | 19 ++ pages/home/passkeys-tutorials/_meta.json | 4 + .../safe-passkeys-tutorial.mdx | 233 ++++++++++++++++++ .../protocol-kit/reference/safe-factory.md | 21 ++ pages/sdk/protocol-kit/reference/safe.md | 83 +++++++ .../relay-kit/reference/safe-4337-pack.mdx | 5 +- styles/styles.css | 4 - 31 files changed, 1387 insertions(+), 32 deletions(-) create mode 100644 .github/scripts/generateCodeExamples.js create mode 100644 assets/safe-passkeys-app-1.png create mode 100644 assets/safe-passkeys-app-2.png create mode 100644 examples/passkeys/app/layout.tsx create mode 100644 examples/passkeys/app/page.tsx create mode 100644 examples/passkeys/components/PasskeyList.tsx create mode 100644 examples/passkeys/lib/constants.ts create mode 100644 examples/passkeys/lib/passkeys.ts create mode 100644 examples/passkeys/lib/usdc.ts create mode 100644 examples/passkeys/lib/utils.ts create mode 100644 pages/home/passkeys-faqs.mdx create mode 100644 pages/home/passkeys-guides/_meta.json create mode 100644 pages/home/passkeys-guides/safe-sdk.mdx create mode 100644 pages/home/passkeys-overview.mdx create mode 100644 pages/home/passkeys-safe.mdx create mode 100644 pages/home/passkeys-supported-networks.mdx create mode 100644 pages/home/passkeys-tutorials/_meta.json create mode 100644 pages/home/passkeys-tutorials/safe-passkeys-tutorial.mdx diff --git a/.github/scripts/generateCodeExamples.js b/.github/scripts/generateCodeExamples.js new file mode 100644 index 00000000..e5d86fc7 --- /dev/null +++ b/.github/scripts/generateCodeExamples.js @@ -0,0 +1,62 @@ +const fs = require('fs') + +const repos = [ + { + organization: '5afe', + repo: 'safe-passkeys-tutorial', + destination: './examples/passkeys', + branch: 'main', + files: [ + '/lib/constants.ts', + '/lib/utils.ts', + '/lib/passkeys.ts', + '/lib/usdc.ts', + '/components/PasskeyList.tsx', + '/app/page.tsx', + '/app/layout.tsx' + ] + }, + { + organization: '5afe', + repo: 'safe-7579-tutorial', + destination: './examples/erc-7579', + branch: 'main', + files: [ + '/lib/permissionless.ts', + '/lib/scheduledTransfers.ts', + '/components/ScheduledTransferForm.tsx', + '/app/page.tsx', + '/app/layout.tsx' + ] + } +] + +const generateCodeExamples = async ({ + organization, + repo, + branch, + destination, + files +}) => { + const fetch = await import('node-fetch') + files.forEach(async filePath => { + const url = `https://raw.githubusercontent.com/${organization}/${repo}/${branch}${filePath}?token=$(date +%s)` + await fetch + .default(url) + .then(async res => { + if (!res.ok) throw new Error(res.statusText) + const text = await res.text() + const destinationDirectory = + destination + filePath.substring(0, filePath.lastIndexOf('/')) + if (!fs.existsSync(destinationDirectory)) { + fs.mkdirSync(destinationDirectory, { recursive: true }) + } + fs.writeFileSync(destination + filePath, text) + }) + .catch((res) => { + console.error('Error fetching file for', filePath, ':', res.statusText) + }) + }) +} + +repos.forEach(generateCodeExamples) diff --git a/.github/styles/config/vocabularies/default/accept.txt b/.github/styles/config/vocabularies/default/accept.txt index fb7924c5..c9f2b197 100644 --- a/.github/styles/config/vocabularies/default/accept.txt +++ b/.github/styles/config/vocabularies/default/accept.txt @@ -103,6 +103,7 @@ EIP EOA EOAs ERC +ESLint EURe EVM EdgeEVM @@ -243,6 +244,7 @@ npm onboarding onchain pluggable +precompile(?s) saltNonce superset textWrap diff --git a/assets/safe-passkeys-app-1.png b/assets/safe-passkeys-app-1.png new file mode 100644 index 0000000000000000000000000000000000000000..c7475065e3ab4716e0bd480b38b4cc08b97799af GIT binary patch literal 50145 zcmeFZXIN9|_CAb#tc)nAfS@9RRFNh%DgpvZq=a4+1Zkny071us1Oe$%1B4m~k&=Xt ziin7GLLi|?krs+bLJ0}^Z)SAPoOAqL@0a)E;krVSz4Pq-)b*@&-}l-Pcl5OnAK*Q} z!NGA@=jIJV4vszA92~pe{I(Z3!c+4xi-Ti_k+X)z9UTpgGk3f_9G%@9I5|eB(Km~UChbd#Thy$?c^z* zgLi6mO3oVh%QSK6u20`&t*=+D4edJZui0Dfe*4LS@_z8JH>Sre@qT`)7*#u^RWoYToafS##M&?frsax;1ttEN#= zbzU`B&*)t^PXS+ui&xIu6ZBiEyj%@j0(WF}4cGMt(wvriCDYq@h**J=#ZAKv+$*QX zKGC)(5oI4;Sd++-#>~TIky@uRPWRlu)~=tQcfA45aa=Rf(9r?jjqJT09Nc}JJbWE4P3Z!cA~_qI`I_Cn zrEKp3mAZG|!_Gk}5bC*IghMq@8Tbfw@V$2?5bEacqa3Jq?$2K+1E04KOP@RQ=TCfH z)y|pSzH>&y!`tDEg489cOXt)NoH=tw)%(7qvf&M_AH{)hYUiAMeLa<>r2_&2qyl86 zJiH%B%P1)+Nng4wefhE^@C!+wAa~z;fs*b%LVsQ4=XGv4_}F_pd-^(ixS!d+?masX zKVP+T=eBS3_upUjbO?0**PYybel!bcp!D_`X&I?Y(tlqYD5|=BRQZl`po5$F4QD9O zJ-|KGl@uu;%FFz-=s!;VXHkfcgSUnU6u76a`oD(eN8x{- z{83ONLlWIMn-JSNJ!Y9&nQNz1*kgSMB-j%vJA%lfS$t$0PW~tKOBiHvam& zzo-&WkDJk4yMBGqF6litqgUJ7I&_47-50F}cE4`OZblwQYd1V-P|5kfyJ5S?8719A zzZe*x9#w))`PE@M_vkPC0vJ@8UpHhY>;Gd= z56$cRLGQQhoGs|~ULOlrv`B>gy3Zb7HQK3`#1F|?dEIox741VUtu5tKa5FR7ymRXR zsJ7FMtQPlT)0~RIp3kDaj+A`$Ur#KW$0GqgCGwG}bp^QagQbQ|_W+W0z*FSnBDgz& zLz7xlD_9xx>ppAVxr;eh5@_T~ax^C$k-ht{X28n8(3tRz#2W;Ae^k3_@utb#tALT9 z2lv8jy``QWF5~)jW%DhwC|c2CI!>*vr9*~TS)-j5gE>zg2>GO0WSEHD#0)!b-VXis zkk6PuazCYHk(6azZbZN-TBH`aLi2@;CEud)2(5TLY`1erhzg#LAx4JFUs8EvaqH6HO^6CU(^pW$Y{4)6&HP-UiHcU;tr+aY zpv(lU?ib4qW_VTUV4G#`MoOS1cs0WQ?3o>r+QKuEG5ztKhv&*B3p1<&Y*uMleutL6 z=laF2wudxX<--EFMk}YIbj=8 zMgiaG)Qa%o`~-flbRZbR)V9J>R7@r~G{TSgb*gWcY|a&9UHayjx7ALGs9}n73%^d! z5AcLpxL6i1t>Y(h_0%pF<$<;)nO{DC8V5a>RMRp*_D;-&8mM7bK5l?5v?+a~<#Bk+ zV)r4fkn~Edms-7v;5ehs9r|KCLPX~3q2G7C=lJJaui!3D)o)icHB}hv> zcy;gDeBDY%+X#GfVV;U@>}X(qZ=1)FY})X{{uJ~IR8F5}ZXB#Ba8R=H&TF-`A)kYU z6ALIm7b6nJZIf2wA(!0$>10Xw=q&s4Ej|xg6K!*ptsde$7ik0v7xZ8EeRSfqDfN>& z&AE}@P#E2%Lq!u_A3fw~4iRc_p(WA&)MEe#G(n@~FO>{$h% zs)|F%P7qr)cs4JYw-jsN5)@tDkW1g$SIMpvQ-ZZg5}xo72J9lyTjJ)0RVkMpbO-Qr z=;CKLMh&!2Lm-5HCE8PHMW=dj{&@mIkULF{&0HDN)-g;3j@6E*_g=XeX&=f%w26Id z7b(d@ZeXoaessGDD;B&2q}+=&v5}2m@(a9FTGuCiFMFlw zKH*86z5S90SLCPM08j8NN%VcOMqtbZgj_3$#@SPhK~gXekpEF5zIrevg7O+#b{cCb zSnQK5cM@KvIOnI7o7w~kGhJ$jyxSm?JMXJbypT1)h?jfhW)f!)w;E9O zx~tz)_i_qYtufokbET6`2Bq)3`7J)MmgcB_{pKR?-`B5=SQ1B$sLcP4w+{N62%#XN zO{a{E17M_!UY6J>tvNHkWE+_C6_Skohx^NLJ?VPin@3zH7*wn+?6dy`ZOgGz)nx9r z#MTQPVa?cyffv-Y2a!qbVH-ph&yXBVkrEa#Y*31EnUyOBr@Jd_#0ytHGr+y?)vA`@ z;z`OX?!M#TY857jkW{B1a9(?2 zGX8~x>Ah*_(n3hVgCc6tie9{1O{h(C{z=gqsqeAjT9?ru7vJpS{0x=bX+#-+f*QXO z&F4|)WwTV-tACKHKz2ffSi|&n^`lR#DD?GiO?-%IQyBK`t!tn!<;LqDST1ly!<0u# zDmqe0dK6-z0TGfu<5v?(#So56^MEscZ;7yuzthUrZjq}f@63sgjpoN~kr2TP4}`S+ zG7)Or!!=7kvi2OG%#3pmm<)+mgM zxkUh3epl3YFOuXIZWe&AkKGd5hw}jHYU&OCN+;QJ`nl0C{FT-qjDB=}5aLQ_cKWlC zg6hLmJ=3Jr6+_`n^3_&b;f?W)%e~Z`?vMWBHF0z0Cf>$h35_i1YbC%uGzHf}Z8tw! zX|AMLgnccZ$lGs0Dedk+Tq7FT8TM)n1P?D_Vf`Q+#ufXds`)ExZ4@ckMy~w2m(MNB zx_sL|sf;G#9QZ|Wc4hMwh5kL0h4e;~Ay?4m=fcjpaCMJVdou^Zu)pg%|GZUYoh(B` zv#z0^>iNc*T?Fw^#^a{LT%txy1a}Qk*PASs-Y+!qPgttpC0sOGy3}#2dYjqy&BCmm z2CUMAoUO7_HLpmO#ed(6{`|%i-`n%#g#fdBbFp@MCAvd#H011~oTH(v1j2MZMIW%W^Z+qEAo)v7T$XL*cp8+D?Q!47=!oT_Gi*O8iEQd=F8ij zeO+s0vph=x!>;1dA_7lul%!x97;_snnSo>4qR*Q6J>Tr$4qu6|n%0i4L9*r_baY}* z;agioUWv^$doX{~UK3<53*IA&6_<=Nhw&XEuhZS8(|tOsTnKgQ;;4T8aLWK-pSMVclBsk9%%tBjBFSb1gRluzN5@3#mBu9F`&f| zyPl0)-+@JK^Jo6jweyz=z*ZVwMRrVszEc3f6g`AOmot_^S~d*O9Wjfa6)!5-hAeBY zVLU>`Z3|Aojf+-)2M%O&r#ZJuW4GNv$8+~3Z z$Arad@)V(qucT&{PiF>C)oylopY2?o5M&Q*#13_^yM;zgmY!N`6C-U&6zP#t*0`!| z9i|w|79J(?U{dpPLZ+kH*J!b+Iq!ZFY^$;93( zGg*&^KFfCUn>Y&!_vY(oxIio#D(_pj@s)OGA$#6z6m+S8>;^KtKAh_4JWFNXYJ{dpfd?t}A4_Gj%kzvyP zAFqX})Gnk-C-PHu%ON<+IWFpVRa*t z)GJx_<&VYMQI51rfKT8_n3a_v!iU5%K5*H)gXm{HWH1i*X?morOM)xnqg8 zlym7F)feC*B9=;dqTGox@bxA^6S;drvWZ_zy=k%90!0HObBgWn>$NK9d=?6PKi1w0 zd-J-}zajh435KdT-uSpJqX;7BLegbg-=$iVrDtR1Ux9mIYd?^&^c+pc#tQI?6XAl`6T8)$2pv%sKdYfOv# z_AIQJEf%&(85P4fdm!v&Mn*nTwI)W$2zx6bBl+(TBzj7eIFsyoH{NM zCI7XFb_`3RaSlV|WNyZ=*5eO=m0j=8K4OY~pBwkm-AwB@;q#YRNX8ef`EQX0lGm@e z2uYF&CZ`wdp_KBOlP2J{$}q3Z{=IFPk)|bO7yctg0wSg?)xACYE~g}_RZ|2??{gY{ z_tAegii%PF{*hTkg8e~QT@li1!VarD$IYI-4Z9MX*sHH1+1I@*ly>KwnvNBP{w6&>)=d7KvFsbLs+R~b4KmLju#b64 zhjF~hh!=QxUz60+e?n4m@P7QagkUu@9nQlm`}^H`a&_(n&@DG}Srz_F{)+6(dT9%W z!K)N-`7@Y$^+~(MeoK$ToL72uzdjvjjbG_zl$%LxO1L0t&U6dX%^`|L=45GS8kqS2 ze%xbbGeRWOth=+SlWs!^`cSRV7B$!~cBVVb@I)~w$6+JcD)Pun`)0w9Gt9Y$;}2Jf zU_|0-ndnB0%C;cIGZr{3_o{xi7!Gt`~Q zJt?a9ai(=%YzzNQRRjnhPTYsN)N`vXlVdpA3DnsM-OLZooexfqWO{$P7*r26Rkt9$K2Qp*$nzW706DN%9` ziI~9-q*s=0Uw#Y>E*%LL=HgS0MZoA>Fa3A);BgHl z1M1Ec|3^JOmGoH|T0_5=Z-|OXcS8v^Qm(gO zHZ0A{`83h=-2)&rD>4^xC)tQ+&raVeA$N|>rMJuV-hxn}Aj!7X?fp`g?U{P3tqNX& z#Wgni)#*(`sAkRrD2b(`*d-VBd=rpG*=^Dru*MxlbqgP-Z6H=twVkpmdkq0q?GFM&5j- zoi&JZI`T)UI?=ktxSM~km;&|@SoPT&ZVROhdlS#y?wZwL7C{jW#xR!E@g8-Rp$ldT zVb|+f;|hVlKx_NBUNbc?%6Lp{$TOlt#2eagqEd+jJwe1`JW5}w+;JRV%$k{*bTTy< z;QTe*sBD|nl4&wZY*Zd_>g;Gdo__W}cZI)DSX%DYm?(Z+yo=)%H`8Nie~G033!P25 z8Y80Jru+`}YmoA4mdGC1Jf4xMdNKOxf9&u-zxEwFqhtYozVPc?bj1Q7w95B#Z^K`o zzXJy71GTgGUqk9wwYMpPk8PiH&i)dqzN!ZB0Q4sV(LXEm?;!f=ZLD8NT3qD+s5>1% z`}>dCsr9R{h_0 z#s6(*|86*bE}LKN<5yR90@$YySdE^r32+|60CTqJPk|Dq`0jE3bY`+R_aFKe+RnHW zHY@gH52xLGG#oHGya3Qcew|-{|31eq-0}nve4ntb73A>x@&t9U*O^-@iuY@hxnl~V zR~eu)hL>9eZOft+f2%aSJN`bxy({ZlP`r6k^_~+}T>zRFyisFPe+58z=`mH1Y_6xu0SPwQDm_mH-MLOy6^EXAi&O~V@i zI9Nq|vW~Z$S^AXM{T8m(8&hf6@k$fhP*u1NY)(;=dwuCwudaH79pW6DZjVfPgo zbm+cAC%+s~U%$HsKpLef`Ml`bsZ>{!P{GggB&)*~AUv{j0 zO|~^z@aes+2*KFz`ROHfUEFs0zUq1l{MA_Bs)t%%tby1HLP!l*clLpu7QNJ|xroB$zI6`$FBbg<7F-F4*?*z4mg0U zS-^JczO?ZpLh>ksz&z;{@$9;`uUcv9RTH$LUBMl|7`=cgu|<>rIP!O&OP>HP*Cze@ zu*P4P>)qqahn?!LM5Z{Vwo2B{mv4jG;;Md7OQ-Co2MBrM06w|s3rQZ~6i}_w+ssqJ zp))FnOB;}(tAiPJ!A2=Q9ZJJ5WIG1ZYWiFmJ#*{bINQLnr^R*jtCek1wGyhwF@y0Q z#0RB=6NZ96UHTa`;8vD?Dii$CU%qK?2FmP*m8m5Y+l0tFd$-yJJcKHbrU6`%cMNE& zY7=0bn=o$$Lum$P`kAVO0KRYJ9)DY*h%9ZL{eI|UtWZ7kw?tWo~0{a$Wfg_KdCWA_j! z?zM?FyW}0G@9L>V*t9;d-Z%KktR-gp6s~63kW))s48!=$jcZr_d!mc%xg2&0e(B8L zsLdnym`A{>r%ay|j|6um2m}DABf3O&VffU8bc3=O@Kp!(SaUN)>)CkE6v0QPWsNLL zrzrXUk0W7hmT&UAHFbkj44|wU?vDoBKu)iHZz{q4{B0~238V0o zFx}vLtiU|*8wWnO*VmdG_6Y>uwGs}iZ|1Qy?yH!H;*}S}9&0oUS#DlotaO6H0QM%P zIZe;DcyycjXuzH>a0>!hvm|_?`xR5U7NCwyOh_+#^y} zu{_a|Ja15KL23g9qS1$O5aZ(&%8nNE1GBJt(sUOf%iu;<^SM`L&v~kwTlz_;T+o9* z5c*f2EI!|%6%B(N_-P~cAC(utS_ni}W6Df7Dg%s{_vMO|s&NYZ9tu;bo-)~9gu?(L zOSz*`@%gge$>pkTn!C>fqQV`;Q)lj+mpH`UT4M9KdAYBD8{(C?V3E6~RuFGcdE?X#!6SDV)Xfn>ST_1*s(aPgv$0ibZbXfTbltaiA?iI~5KHd- zm!1qnTvoYcDxcAUxj{L=fXS?7!LaB7JfvCVk)KkO!0G)__ak$bod0n72 zivDcYD0?BHM88dS<@1ET>hgPEzuDk+OLS16V9?}Iz<_lo_UkEDewR(gw+H{d=&g5t zrZ(~W{EJoX5|Ziqn2y@oKX)&>2|z=OT-2{)x|s3mUMCn-55Z2RM%rZnUb-}0820?u z)Uj~L^teYS)};+Jc>xO$uPYEJ=>Yp-L0vRE+#-+#tod;JZP+iRF+-_$D1K}I@IA|7 za(CIMsYaTR*1^p=L3LxIX&5#1o;CXn5?!;EL`T=CM8?Du1pLJT7~?xr+V4z=6|V78@+Te`oKp66y2s}* zA!fCGg-)W1%BLe%P6U#^-?=LEC zz$YKT8&=QV&T3|nH1d1KJ%&3epD>uDd12dk%em=5QtGh$PNPM-!xe9alxmT}zX zN%L>G4Pi?g7F}Dra|R5&aWhQ1XK4->h=z6wPA$}@NRdSVT>w3DM3Ipqr1XhBjTjI2 zd%x!|DauuF3kQK1?Df-(Rhr%f_9FjPqhgVoi~*6(1I136ZGi1=vxd#)-Gkpk?)I(m z8;B|VlLbAv7GbLY>e5*_pN*?N`1De@wjcnDPc!2yn}(tRx6*nn1W7Ie7K`5ptZ1se zF`edl0Xa}Y&YR(Zf@7TM-KXKoK>(pfOHygdUpS7f3l z!Ezc!tgUJpon620TNT9wv3tFUt-v3zB>DlzvS68G%Qw@glW=eI$hfl1%P>UFpfN=TULa zx+BnfdcQlMSM$`}DKEV$cI_>EVwBw0K@OwFCh29#UJ0&OkqNEfLjyfoA-1 zP|B<-SvaTH!Qkt9w%(S%TS2%{vnJ=R@*r9H2aXnUAQv)-Xk>>k5!ET&xE1#8vEY?c zo0M*_gDp}=C z^xw|eK|o!L|2;nchtdgmYT4t6vjowy<&uF=~0h+tt;C)_HHjzSNP(@c!B)$(Pt!p zM)ffh3(82^=3%VACd~9ZnMS>b=8BrnP4oHznmu%HQ>t>=X9bGe(*oh*oSblF?!-m? zGlcK5rhQ@kf5)t9l9yf=aDH6=dz&$S(}^@I(-qpo+LbZ{km>?jhfLgFj93K>`$1Y& zC?!Yp0WjXqNn<}Q#~{jtl%)G^+mX@-Td|2)_*Z6nVn^p!=d^>#yzNg_AJe{w0c|&f}KW&!2=kx1>sa9Tv?jQ z@!RrQ8qT!);*rRMm9Le6Zw3|4=72b?2QpS~*Pxd>0v_2oT+an^wVr13zFOQ2eW#iRf7R`^UtDj9?sG5jQY4mIS zc2C2_Y#M@Z#CYH{a`s)GHQVxckwhZ|o>T%#F=JYjNI8109^gH={Yt{xB|saK@o0ZQ zNcJA>*RHPGCUUOgw&Ji{?MzN&d5--I2e1=gZ(@0@KDt$|)AmZn{ygl<#ROO6>c?DeInhswpI@-xffTNJ=D{|Bt)yjF%6e#rsdvLEINzCZWF*c7x}6^}=4t&ZiW>Q4z?H>R`u!v$c4X!msf;d|ky%MDOk zPj}A%?vt!qHwAR45PceFZOqj0xjt-yLtPQojkY*fJyWzim(11Vb!dY!99fgLVUWsA zcM}x0K?3ID<-^1lARbJ`3Zyuoo16wv2l zv~}}rSGI;YgF4;Yj)<#(A3z{rkf;sr-gtiK*9ZF3x4aYB)8_LP=Xrh%FPVRr)k6i_ zhlsHEJtxLve{3W1_F0Lo22q!d$3-QDw4+Pb!>H<;^YJG8mnJ4>#=-?{%vwNSigF~w zT;ccXWjn42^#T4AVtboG!q4!xbZ)-d2bZ7W!kbIb%fyL!}?hxWmL^1kSU9t5F9xWwq zMMu+d2xKkJcQl7WA|^4ip&s>glUaJ#!e)QCR+1*+Q47oDj~=qYyFi$(fwwOZ8Da)= z<1qeAK(JfktNbW6YCWo)@ACbdBS~ZlF<`VIX+YRiBpHWdf;#Px|26N8nLW4dk=_=2n9g*UYxk1R!E89%p~WxD?oU8;6?gtSS_(KG|Y`Iq%iI zWl?ZHEo|nFrixeEw#^wVy~hF^eCrx)zTk|m!5?IhRdMBTRoH6H=a13s7SL8%PCX!> zZB`&!G4?%!K&5q~T_&NJoT6PNSPOu({D4CzyBl#SxNBo0KhXfC>&BnXz*5hvuWB;a z{!-<3<)7qexcv?CpKG=_9#sQn5KV0*Kka~&h}Sx(()*oS91&BQw-(CK4{`Xb5!nhfMq%mT8fpT`fpJSmezoF{2`Q#g#i(f zY=#F}zkZGPl!Ys;&^K+21%zIRTdr= z-iYKvDZ3k_jQly(&TDUk*)Rtu^*}v!i)AqapfEt@bY*{!!IuFUm z-D&MCua=Kdo-2H5tjAdr8>`vU_ff4q%-qB`t=^r2mA`Dbpzoog$vNHa)Lz+X>0a$% zZdw7TIV*FvwlU#`OB`1ETr+V;IJ)RRK9H`zg;SxpRSl7=XK=V9UyX(BbX$N1>RKfqX@W zRs&o$AgoNfP!7mNSz^-d)oAC#2!Xm>`-^a53+`z}T{5-! z5F=P&fSkS-fEK#j0f=SsF{irc)_!WoD0$d87?`X@kNb<}~ z>%-v5F7ANlc&v^ErO8t)+{&8Mk!`&j;peK$>{}F|2?~qa`O54&!qXmv0QFV@MwaUx zlaO;gn93gAj!q%!6tFI)j}0sW%iasvr(do3d`Md8x2FPgn_8ytbLMAobB61T67Ril z|H?xCII+l&0^p~LwdK_-%urx6h#GyTYN`c($6Y9isr5YhIx2< zt;Ub|QR^~O;hk!0Rol_EYID6kIQ`gPkl!}$H{-Npihp6It*!NMSMFyc|3pKtrf~Q~ zC$+v7_!)u!qtZZmWeb3@*0#0LKl1DIpMbjkuk=6W(w~F$|97uBMZMwDlNg%*Qg8kqMA$80{9+9LT2hZj&R+sj zGiqXmv}=HMS_63hpF%gjCNBbkjIYWp1`46iMAmCM>ldP2R!d;AnBrN zJF~^A4alNsUjVY(AaqA$sDA+9sn>1?VgU9`-sT1tw}seL$;z(li$LIekyQ-{Prlc% z7MgP+rRD$|x3nDtF#AA&gZz#fYx+$(;bFoo;5G?h7kgZ-k$_Jr+6V{I9Lj1ZqE&`~ z37+cik!9$rsvHnAp8S?N;YkjzJ7N=GtHdz@D1P?PYM9}5do92 z+6s4SE705e;zyLcU!?y0%?Lo^1aR#h1LD zZj=Ik_0$jWwESZ%LV(CJ1>8rUk=g8FZ*E(2Piq>oMIgB3U8_j;JNZ6bFmSWWNQt&> zNcm1!q$Iwua1+>J!s*iWG{dQ5mj(gPr)g8RQ&qNa1$C|hNG?LpwTSH2sNW_Qa_SrI z58Sd1>11~G1CC5Q!GV%$E`=Yk8w9Gx zLxyc_OjiIthXh-7N!jpy@mcs>|Q(_sJ%i zSe(oc5uAW0;gF@$DmE62qKByjfI?#O9y+Zo zS5gaP%B=WVLU9nYfM#yfNh%Z5mm)}fwGo@UQ3C)e-H3amhZP309Mvd`;^8CkQ-?d5vlLVgH!&SjZMvXg?6MW0s43oTm`)fKgHCuX$x$3D*RJmk5|_|{DQ z4q{$;FrEoS5)7Y1qCqmwJ-N4Zvij};jMCx#(c2*ubgRGP(&g?^I3JfYxlox5j!0D; z2^8uh{njUv8F+4G<+4d-QLM9qodLJJg%^M}XLkT-=ygi=4z?g}8A#%p+;pYA|5*89#i2{1nXA@IfFrtfsire7qTdk{>*pV>d&zlsG2(?(%}fs3o~S(Cu0WBX z(Sd}YwzchqRkMPX*IRQZyrOo}2=At;Zk9FMDP7mYjwnsOy!-NkHc+VvHz5BMwEEbeb(7Y+Ip8z6zJ3sjse#gg0{TlcbbJ{>Gb`Dlx}(>t+2aa?&7Tc%a( zVw{V+M;ZsJd`Re=pQrG$972E7B_S7qR6axTYB{~aIA5%JhRy`nPu7>O{$%jF8FF6)2?X}*l%AmoV)0#J53OGk5xxNg zH2nyFTudnnX^D44Cf;uZ{LhsuZ~z^xSm%_Qt6-pN4;`NtV=%YVG`SbH!@{u@j54Ep zO^cCXO&yAiLv82A%_Ju|hAuasm=-GF4qynyhPJq#uj?2DOU98%aK;fL4AkOoHhR-` zAy8sNQRqit4|={s${SSmk(+&GJR~9r2>50?WwM^+egedwqg??gRyjJ97Fmj1xxBI> z>ZtpsP&78{20BhlO!wxXD8KNcae`c}W)N$|YS|;f#qV>oR5%iGVLl*DfwAiJi)|>{#fh zU2%pirOP998bhc9{t_%MY>p4!cEqo?_S42R5=p9HEF116gQv6Tu#5}Z6O2^`;Hw>6 zpw_urcY6c3I|vnvFt4UuPc0hz|@Ja58dwL zO7rXt36X8rThH9qxoe`_fS4PeouI_!&K1XfLMLC)PJHv-XY(2BxT~pcs9$EnCV=my z5uh!v^SDaI5PO7cprJJ-Ybh%$NdScdkm-$GHudZJG2D%W&_s#eX(LUNROvC~CXmx| zy5X_JwJIQ2_(@GvU-$IQ);rvw2j0&Wg~o-gH`m6!fpfM}6xTJi=tY z83d|q)uht&+1HX@K{(`Wi5q)<_j(=N*u+2eiJ=alv|1rj-#^*Sbfp? zxp(5MMo~2%Y9_9>Kv#$8`^31dPf!W!vSP|$dQ(2D>!Sm)+gM0!I}f-arauNquPq@3 zXbFYlCdwv9U;NyU&+o13VohE*W$udbxv&UW&#cA|5959p^8#}7p0eyp@0(bG8%3$f z_M3U86k22hLReYO#jw$>g)k#tmX%}hqCb<%X%=8U9-SXMu^MO_b=9z2dI^xK8XKB4 zhP0qwS6VkRTXbpy1?%xXeTCwb{8$Gu-6P~0tqyY?L2kap;`7m5d&N}r`DP2>XXvVu zJEM&EO4oUg^aK1cPsgyK~s&E!Z|{$ z^6F{t#p>y)X?K!EOQrf>GpA?LDg70`o4hPu{h|e9VR*m;Brly zl`*Rgz%+?_C028!KYx5U?xRane}Nv|yg3@X@ZF%Sd!a_3;no}`QNFZKI1v{U3Ti=K z!x;Txq!UDRo*G(+QDt;gfT{*>uC520FSy(&Mx36;5JrIP(fhS{s%*a9bni`DoFM4r zk6d*c>f{%@HB@9f$a_6p05Ddx&68cC%87uSrT*-}&1O}7j@c6q+`{d0c3p5^N{()7 zdLO1y*Uou&lDo8Ngohdcqln$hxe<Iv7#eJ=%`3}RyLkmv@$^G8s!iRpddL3RIV zUY%1UgYuiLdoZrF^!BT$BDu>Znyov86`=Ry-Fq>Lodt0uF|VD4K+>ho4r|Wfk8#nl z>MvV6ZWFp(j8n+*)7zQbULz)*TDvUIwE`VdQAGb;K=>{s;nM9jk{>=d+B&josm9tu z6Lkz~ds{fm&IfAYEtS##R%=nJdNOXK!3AX!A{Wn3EHtIYOFCc$>%sA;)iZMRQ9BF0 zs6UL#&cDPZO38~Ep|YU4xvSlhYtHqKARqt3e5_k7Ek<^RW%q^BaqXq{A#HV0ZYA>I z`zSx(y1q|Z*_4lDEv`f!#iScXqw;o^nkeZ7Rm&89$T?%pe#EX&@PL<{%H~jt17=Ai z^ch4Br#+Mu>ffhTrw*4CiJFQg&Eo6~N1K*m!nN}9kK=NA&z9zN*P>BSh6J_N1f}Ii zfz@Gc6k&@z`xqMfAsGvskg(^GAkb~x)4qDMUWjm5ziR^v;Xp4WB0$H0Nq%eNB(U>efM}D9U+F5Q=MmZ&U?*xe`-6@6x z@@+Hw#Ac=EmxK5LYrau<6PJA+p)1tdz0Oa}cW>+eE!VNboPb2n^m_g3c@k$mml%qSq&01FGD!fE=4xCa`{#9RP z;rQGK0=B>p6CKAlt02q;?YnPKXMP=&sc|RbD7PNmEKVG1>GK>tvrg{6xjk^W_jf|9 zqV#)lJvDMlyrXmc%1KO-+v6V=^09sy3rTiqCve5cYAsN&vmk9mD|pV>7tSac7aC_(LlUJ@ApP6^~-*BQ~=rr4YHN&oLej-p0sU6_(r~ukK*sLccni z?8^p%dh;kasjxD();^oL*xDGmO`dgp4M&}JYEFs znvYR|z8`!tZ*!HqUzHBq(3>4&Nw0Ur-1mVC=ugKV9D~iI9y)pH%|@<6VB>ZYBkLO;Vlo8JYIfk#}om0;fnd#5`)Mn{zw=Tm2V zWGHSG!oj8u(A;S7q!RK?=}>oLg&85xcn|3M>l_<~dC|Z?jv_s}s51k(^=3QK)Dr-= zuq&w9VIb*zn)#6`wFcydaINB&_PBl&nO<;c&l6->;z9wL*#1#l2D|y%zSGAWnplrh z(*@toF`;u~D!P>M9Ja@L?CZsCM0y3K{g@3r9VFd0O{k%J#Ujolo!24$uCAUOEqwj9BRny~5sC{Ee;dT4H015zzY!7GXqQZMHqn(peGM z^@C4{j^4p{G}qwDGJ8!n#P&|dlFH)N3FER2ahyJiL*7nwLA%alQ0aqEg>hr7 zkBOR!Hjz-hEO+jXCbcg)aJSLWiTeJ+n^8}0g89=KPfD7-d1C{;{>0b@?DG&k@Y|^GWMQ+G zs^&v-|MXjt)fNBoi8<>?zfmGpl?-;oMrBp49$Lg_RV&>5W9A`p(MJq$GuXxpqDM zgNB5@;(_Vz4r^2pCc42#PPuCeBK#8{6&|;uZ$Ev0-}DaCh6p+PE`wST5=Q)r$q`Tx7_&NsB3{ zPq2N#h4ZNxg5Vg3QhqpobJ^(#j8VrJy%kajMx0VRt_6Ng+5%{T<}^FwEg*iuOh&Hi z#G_z$6YVVgXeqstE&gr(nERFO*@df#yL+tS{L?DPb!5Sa=)jH7AX~*|NPGCRcU?rCzJ6ORXpYDHoY}_i-hA_dhTh-9HzJP~WGVA4-se zEi$BtN;56wuIi%o?&YyfB9LU*=sFJKnhkDYIQ9Au1S$5I7PrNP$#VVm#EySW4f~Y8 zrpCjQ>!cx%RlV2n!OHu0R6D-{>QCH7@P+sOTlm&*2LlVguu--*CicA+?Y_5PtQb|T z0HL+vaCxx^hNi7>=B9q*OUlqwa{i4-u4*rK$ze;E%A7g#?&m7P^De^LZ5;-o<@+w1 zZ_+yk=giL;`-~1MtE^qQ=R)B%pR1$2%&jcdaxnixV;bWFiTe;2XJX4qEQc3ml8vtG zlUnBntWZhcUelu_=-*IPg1T?=I2|^Cpx44cyVX^1e)gm$>D|;};Gss)6*y135_&g6 z?V-+e!P@LlY1D^sq~jT|#Gpp5ue(B=erO^RDRl<9X4GPQ`KC{MYI0+qX>rB+{soJI zS1%&D)MrCt`=3cB?ykHavmkEImr@Lif=^WIjmBo%>I3t_>oDlZhp`eRpMcpWbhVVK0>*jd?>Dl^qKsSPT zWq-*E?tQRR7h9WTLb54y4K=)O947HPhsqT;Dj&S)642u8V@0ji^SWTqioHo{M{s>h z`i5_Cat;f8uAM+xg@xi^|AZqVc1Zbxs=GEoLsx9=wxP1;k3OC3!cY~wvEmB6!^cK- zQ}Efg&*f8%2~`D@|JUA^heNrC@0U79yV62g=IDevDka&+XmwgpsT47liV~82nVFoD zl13#IF_ki8nIucLQA8+XC(H~6gTY`nv;RKRsm^I~e&0X8*Y~=nKQvb_Z}0nA?$7f) z_x;>@Im1`)EuJtbY5jAvW_^vK;(3HnF7xiTpkbfF%p-JKqIWvFX}{^J7-i5ZnTNwr zwnVfvC&s*cU1qw;t~Qg*wn6?B+g02;F1s$_1A?ym;4=u37Xpdq%ayVcDGLVHMV^ghjU{;m~{BAd6cGHgaZiUJR}GhQz38JK+d;D~*%1txu2d`kn6XSQr^Cia4e6dr_IgE9J-(sI~?8j@2mtFlbE~SuJvr-&) z;vfIhn8w^S6Qk-2q)5e#n}pJK1TV78XYSNNthK_6$z+{G%)N@CCx=0Fal7d%IYg1j zedZBR=Gd?;nf6FyCO+?9sdQl_mV*$0anstLj;5;5iq5ruhx5m zzM+Q_E5t59Tay;S(8pJg18%ek=xCnxx&ZMAC_^TB14k#c!>O?4d)Xu079e0aPa=T+ z4o--?(Q4^A02pB5F#Ff3@LC&^94A9d?B#EmLlfmZuG8*8#D7 ze)FMy{Y8LvW{uh5c`?DDa@hq;DaRzFb^eFdx2}x>k|UuL{Chb~BJN#7){mHwIKv&o zIi%)_fWT+I#0GFhet)b(AzaX_bm|#phXS+@IHnzr({efz2k_0FbVBOl(_fg4R<{9Y24G3t61IP*RV?|j003^{smcN zv4~Ud)$awuieE9Wht9?x*E*2{WNyUIslDKynJA>CNjyqUM+r{EAcGpjx{{WMBXGoD%?9DmA=_3B zSLh%bx1fT7@5;4NAYJwySc>Ha1#bpi^F@hvzzA(~a(Qcu$o#nSuoF=2gUGLu?1ahR zdgjBOkA(NfvmHH~>$0oV6WVkNweq5ed3{+WpW@87*=3`WtK#*~A0k!GlviEcG2S$* zkH+i;m4xa}_U{%9ck?$7aZ!BLBHqrEyVPJ2`ed@UPq_ii<>EkeDVH}DW|MJrgdY7S ztZh5rHG$>)ra!heq*u@550ntJg1Uf5JC;g|29N5fGD?pgSkAv7+&+K9vb;p#85}5_ zdJj4elQw{2_7k*C*+4?bJ;RI~B;Yh1xOu=wGW&Ew%c;*r_~-)j6Og~_KR?*6N>pyn zryu)cZa|hl(7wk!4fJ4Z_iJTlSFZXDs5%v-U{1vW?CGk(+n@;C{(i2R<|Yb9V#k&; z`-vPe>P#fNPES*wQrMBY^BJoCz%FRX!J^np4ktr@(bMejRn%UUONzR21F6>LLEHtzMR6LwOE(X{vsV9o<~ z6-`Ff0>@e`m$><_-YUrVk~^;uA4S;{>NFFote zYfx^%<@~DSYc;d)pR|>0fvtl!%{i-fi>rkS_W$$#`6B#TC>+-iK4CsVE53(%vg9t% zy&ug3vJ1O40>NnKDe*aUF3>OzSm}hOIuDysp3cTZ>i^Xmf!8d4joVdy5S0+*2k7a} zu~TUkX^J;ZvJ?|Vr;eU#PT@W7dmaOdk-Dn2pz4(o%zX19OqY4z}aK0{ey= zj&5;h=_2)B-X+98>id6t_=ciyhmsJdS1nl;2kg2EdafNR#$ax}K8-iZ4y;*+Jp&MZ z|I`Fy$3W5Z{BgJY&oD)Loxq1`N3Xv1j6YVUDeTDv99Tfhhn=S9%vK$&&VQxgK-w&R zr+DcFje6pfp+p%Z8I^uW#C&OC-SOt|=J4;%EWQfU;2_9pWxgANS@&KH85 zI`wBkC4cy6fTmmjN*mb8w(ZrU1;t>EJ;_Sh_Ojt&D+!47Ctnnjfa=994c9egnW~a$tR$I{)}2k@xnMKT639QDFXcxk&wG zVOeQGH(y6w4T%F?hU;vOw#@$!fY*#Ywwi2HoxM9^w-P``%@@XNZYhSeqBSc(A!KV~ z_TAI@3@lZN?#4L#0*!J#SXzDW#wT?eU?4wswgV<}vTb6tKOX&C62|9w%+VUFrqb3| zhbVhXdEu-2`8wOw$o3fjlO6L-b=54B-EKv&^J$NRB4S?Yo%{G;ljC2*Eql?6fV78B z0hV#cTTi^_NsN_8k{t#zkF9@CO`c19=KG={aYF%s(DY;#C=nHZ)94J0M~;o&PWB-BmBhnb=>l|aFN|NG!z z%F^pS&oSXm!ftuk%a*#R03T88$asaHT2Vu*I^|R~8hhY{ z>>eYaOLIk-^8v|# z5pNmkfw=awLEByk9W+Zem+*$udmfN^t@)EI_B-wCiI5T+Qp_ititns31O}2X_iUE< zpB7a?P+f_j=UT%$A~2%|$@kl!zyqKL9SICp7#vpqF1g%ePa#X81$)^fiYg{!tHHhKJ7gdu6{-FWLJ* z`!M4m*l4?oN;+E4uZ()#jGUPBC$SLc%WL>xLo~oOTuaX?yl_k@ihYZUX)bJX>96g)ke&(jb_?5Vs~U1A_SmMch`pezW^8h~??Z%1kVy%>nTGmd5MBGbuG(IYLsiEd9n3~WC#Xf`E zk36a|A91uo4;vt%cb&W|Ds5u(`%=}D&hFa03B*(57TVAo)e-kzoWEzU@S@RutkBx4 z;q^kV%ZIZcdq?1I4tLu2Q{;8mi6kN0oz&aTYphAMJi*ha-s^Lm5p=P2?<%j`{{@{l zLrc9@@6~#*Ft~27(a_M)sy(i6SGL`ZzS&_81pMRIm<84X`|1)&nDu&V5OXlVw#RKB z`o$& z&8s6S67JdUjUPT`eEyRC>3y|E4_#UMh2wzX$_QF~2Se9uvTe#tQcT-h4E*nu>^Jic zD=d4(>u4jaTWN<2n7Xrjb7ej&{ps$?%KQ-rhY3B8P3D+aeW`5sfnySVO~{Q)=>=*q z>#5Dw-E~hbI^T*X4(&u!Tk7$Za3$+0o=6SbI>C+guChPVIC! z;Nt|H;HdEitdeG}0G#Z^_UB&v{Hc@?hbK-ODsFoH+ zy)UcUin_F_PJ0K%ia6pQc6=nm3184a#=UjMmCo&6`SF(M3SAbH-ou zXU<|C^wxNY2ReC!&x$S6mDLI*MKk}@MlCs~G6HN^aDTy@FjV&u<#i*FzWX@JciWDC z)qX7av(LGbpBU@OCF*ojO&8|ZxkX$7p=}irJf)-uVu7um&)9MAb3enV1zGWApDVf0 ziMe9Bd~W7!`!!`B8dhv;Trf>C3xJrp0hG}Yw4eruX@&vZ(e*AURmpz*dui@pGawtf z|KGAHD_fDMPDb$!l}(IDBGPP8J?EEKx}}#fFi}c^03TIkvm4sU zvAMBjx{TH}t!xI_zO`{h(Ua`A;ernfd8}b#y`B|YDKv_JtUye`^Eq)%{bti`#Z`+3 zS-@4JV(ra_U$fR6xiD|-^=12(O}o(dKW@{u^woj>5_)vb;(eJ1o-O?O+a7z*4nkZ$ z>>YV%v1{0j?B;p9I;MGI6HCdh0;dj9XFC)Tt{uC;S>8SMuVwRfDOu1J2U*n(r1!f# zf2f*&-^Gt@K?&NgCZ^BqKd!F>JJ-tV;D7vH9}@}hfIYFNZr1lZeye?dxnDeJZ0cC1 z6f@npEw=cd|MSpLtCq2sHJB5^NIo-9sjGWdtM{xpG=Quj%({!^~7Mr>k$n|5pl!{mt38niHN z`1 z06X&qD`T3~0ERMuF0-?5&_z6j^oL|(I+>_%gJ3Zo-;3O_0Zy(%kMwkdf<%Dv(ve)hh1--Obf5`u|GL{ zjJDwS1$s$CS6(aE9(1dhTeNU{+2RF%Y0jCuMkC=2;q%B7JlXTJw#JCo=Du=dJc76d zWQ!wCZNkFIw%{>7vQl&v9+S(O7|RKb2DU#Oo4O4T-PK> zdJqGSj>hIc@Z`O%LMMnB7^|Mi7ugehNbb8r#%;(-!_wZwT0+~S(w=b~eEd#7o^Q_M zdI_US162#WIFk(SC_CK%XDZlg$>kJQB|@!D=&!vf`^W~Z(21_>bF?^7scFclU|y>V zoXz$c->2b2>Sx-SdyfQlGG8>sl_DN-(&BVI!^omSh4uU-Aqk_ZofzEFE*!IoR6z}l zQo5o<>6^#iI`p?)sx$cjMsttuSE^CC{d7Q&#X>6}u#{pi6x!fuQ0B3F6hX)xp}mhp<{$GQ`1YLd}^G z1@j8jq%|Z_-0eJ7Fyb-8$Q;sSxigVMI4(jbdX}8-V%|cT)p0-Ut9+5`RysmwE&X9c z{~lUSSlL`|O#?UPo~f9>+_}jcl5?$QAwf`??Ht4!yjvysqsBPWgZp`Y)f+njEt^}F zRG=tFwL4LF%7+LG7&oGp>pDH{7O?y|?YvurW2I+0bp;J}AwwcUm%hhTT0Ytq+1FY5 z#!i$l;fv>7btXWBtagzKM3pA)*%5~$bMCsuiN^v7J$C|WXE9I#W1sjOcR_=5Sc4E# z5;%hArS!)K;|R#8c6f)+hur z9wzjkmrM;A$5sWRgs-xPFd?8NCX~e`4ZUo<@o(W0qz&;4@QN2 zwQw@YT)xiuzQ^|yY~mh?=9?QJoT$us^}b}kx^`if zP2^UEF8u+!q%nlJrMB~sH1$8KNDqQ$gbcCABz6N495%xi@8!k^mQCo$0}_ zlHv{GZE?kyxVc;5`RBQjEe=i$YAyAp`Cy{%Dr|`$t(N{t+`%tRuVU$jFxmx#BqM8X zL%E+nOO*=C8RC-l{De_a8$3Cft79Yu!D#ywBI*&U_Nvt027_s_v-?_zgAgtDRNWCrBp5L;j#M<#l5Yx~=5f~$X_G{>2peH1hiUG+pL zUpeAi%e>Io(v*IHFx5hhWZ49_Px|JNN4B8WlnGc2I+^I*H3cP`$*@*v&p6eg?qw&OgQ{d3p2#bfyF6N6 zr`!qlLcfYyCh4`uywD~?NW9Y#6Z!IJ^+Mk_<)u2Mo2-#F+}bG5MEGc+0>nWq+)|lK z=QEedk#i@tTg?XV_gA@;424$;<~4D%={2+zPetD;m6e&mZNzyIwn#KiaTbP=Hz7QR z0&COlJh{0Fk=!lYsreruw9VlFU6L$=yO@}biB4LSeDb_#_hD^l zKy5&Mk&`|fh2*zS@MJ6|SK>0a&Yz=7U0mD~j1L@%cbaS3S%>+BwoUpl`OV_Db(h|i zWFn~!u)N~Em&^U3rvqzwf;<)%^ZRBG4n)*4c{~0b(*fC!pp+^3MULWxWi^I-?Sn^<^i;b(^(vmnu zO%6k_909Mx@=p$|s0nYlEP4PD`4p~!1gjR709)*JqwGK4T1SjJ{uX)ea|9!B2MY4Nx+O0a4Si37@ z0-{2c6^xk)pGYLOnU660bD;hLc6eoql9`{`_(FoC(?VnXUp`13{-gn~g%R9jw}E_Z z=sJdX)$3Cj&cVZbQs6j(rHha@5JozbO5crlo0sd%GmWFO%@NvDDsT}xn_&fUBwa7< zc}Uhb5Hqwh1u-f~EUSIbpxn9Cjd%lad(rL#jP$+q$<3J|8C7YpiDyF_M08~gf~l(u zf5qK}LVZ33`^*lTMCVTrZBtG6o-x~mI7f{iIucXGv!39b8xN&n104)OFiItkdUf9_ ziXAjYiAQ1%(!NN|rIwL{iP!wBfz2{vRZQSK4iOZqpH<1&nK=6Pj^Xl6f+L%@8SkDE% zeH*4Y19Y~_zXGpsCta6q=+dz*mh^RzIDW^xk7VZgmde~F2Reqo711)%#y%du`=@PI zzot7T*{aPb)c^Bjj#5$?fn=-wQ)FkPF6U$%WY+v?%}DuU(?GbE^THY*LI7a^Lq|LHKp%U zW~XrgV(D2T=)m<3ag%3zD2Vp|S;oMrqDX|uM#lz!aKcBcWLiwQp3d}Wf{ zbb}No*X!DIFyrTU1*f%>h3Y4jglXpMip7&FM)m>Abe2prVRw`kTQVxO{yl}?hUmcB z-_R@6-|w9UndbEUQqfSHJ$JfG%jVrk4zmQmOICX3!21Ty^wRgdD0{W;b& zQXz*7Mr1G|SrW29OBy_7ftECQ%JMZS@Ra3i(%>nB5gCj~M&SPrwIQQ3WORm1Xz`=! zLw2so&b2QiAS)n9$H1}zf>aDFDnB5gCj~MnD#5Nn?d9(2~XqS-vKP6|#IyI^>YShzv$#0^NUDfbYFsvV86P zP<;I$%h#mAQgZS-vKX6|#Iy3M*v!nlx6(@-^v@ zLk1)N8!#fk`;9#Kb;gYAr<75xyOyMdFY%cy{XR8JVEW>DF;qH(0E;>|UF-{L=`x+gxh#c>(Eki#i&EYVH%?ti|zv!s?d;#=xJPW77qeD j7*hMRzAoW%v-X^NTrw|qQPd3ZXU{I{omtyY{`LO=^35RV literal 0 HcmV?d00001 diff --git a/assets/safe-passkeys-app-2.png b/assets/safe-passkeys-app-2.png new file mode 100644 index 0000000000000000000000000000000000000000..c5d581dac8b319f29db3993aab7a2b7303f5b3a1 GIT binary patch literal 90818 zcmeFZbyQT_8#j&!79t1=N(e|PtsMIioNDe6|APrJ7v`WpukVBWWbPo)r zAYIZ8zs6_Th&&m)+kPShf%~ zD%h|dD1*(V@spjq%68UlN^iSfcEhF$b>>~>2ev&Hv5yvIr;4E8np0AA+?Yg>t*W)G z6zO_y*0vuSwAvL#smRdhNKn_AZ|jq9F$*8qpPk8P-h`cjxtOt4Unh5vFV|sI47$pX zU_CZIY`1)iPt`QL7{T(IL67L|$8ct58kW!&8u4$0)2W98A)YJfrR!4KA+2gLRyIy# zXu6b`A^{EWBR2I`#0Pc>Y7F0ELuG>xMJV!@B!WN&KK`2_$C9N`^osf&Jt=R zpZ;}xy7}ZnDK!~+dGM|F{H2MBwVk<*J)7M(R!|fcs;+IXt)wXY+{TLQsj2|Ln{;er}WNNmezK{&SE$JxI!2lpZv^ylm3rO>|tUzwUt!qWo%xW(DQTg zaPiy}zd%n79Vjf1_|&6_6=`u+E3Jx!dU ze?7_C?q{<=1G!JmaNptL;r?AVxK;GzS7BACvx%k7eW(>M4|s;S5Wk@4AJ_lm%wJFZ z+nw5f-FfF8zrf#b{oASk^HvQzlb13!R^XZT;(v9`&zt{#^5>1B+$T-{n<@UZ^B=zg zLyKP!<^J7i;ur2ywK?PAN#e=hmr{2=wU}_Wgz3Rh5$wyma1Nn!Z(q^N6C9a*@jgdM zf9_p4lU!Kgt9KNqF8kNMBOr*(ctglk@J{OPySjIFVexa-iF?k&^N8hGAv@EKj@ZhD zvT1uKhlcX8#NPFdVGWshyVzq=dc0HTB=PV`U*Qq_pNGjL1dR(ay6>d_{q)JNHCO^t ztviJORRSJ9C#j^q)hQ~S|Fi7Bilj$BJN<7@|MLo6`d1}5eBS*3s3N#XQorHMf0te; ziD!!;G&lD8XY+nm;Z;ch@qd?Y?u9pDLDbcZ|DRUEv%O0FFUI>>dKdnw`hJS!aKeAJ z@E;dVu<`$2`hXDsEs9f;k?0@2^Ya0Cr|LB5k2AjwLaLSb`}m(7!Abf)*+-PqOy{Z3)p+rC-_sqzRb=RGw+)Z?VyMMj>~{)vO$&Bg(s*wL!iq$Dr2gw^qYsSU+9>mr zDz)SfAg|xOue7KamVvx|ZXRBB!@f%jP5vx>zGAhJT7i1xk>Mkvn`Hmh_16{%gr`f{ z!n^C+HPZ-4ub)qLCni&jiyiLoDbzEfyw`qlkbR!o}LZ_ynER%d@($}-T1Z;pasG-WH47PQFwcb>3&oo!g? z^n;woHL}J=cSJI~lGkVg>5vss7bVV+iB!9h8a+^Tr3GhVc(LeObkCqs>KvyPO;U71 zQB%io5oFi4bn)N1&+B%2d`~-1-Noh1^u&3=i`m@d3zR*A&l2jkQd8>ZgvWA)uX5x! z-(3FYN9Snhv1^i`Ug>E;zhs;YeUyGLxO1L=zgKexcQ{biPi}+hQP-2GdOjvpcg-!$8DwmP+tW%AegY#?<**_zl3@0TuV+}b0+tc zCEnk@yq0slhONsv_kYxptat9OR40jTbjb`|mZiCqTpcJIbnP;Z2di&KY+iYoSLZYa zXKg-D#964?Yvr*!6;nWMU6L}lVPS}TDt9)shqI^9BDrTfJt=y-bSAMC0-uU%$>%Eh z>doUD$W>-u)T=B)$nP}n?quS^?!;r=L~T>CxY4W932|M*ETVF%TEveJ))#kYk~;Zu z<{uG9{4mxs3Ju8h(CcRLcXAKclaBRWsyIR?)YBTX=iNLf;`%3X(AQU&td3)#1(ZFY zn5$Jho2i+dUT0|rxd%-)+0gUe+^3&tJ}jY%ka%Ra8%Nt#mA0Z#o^4sQRBZOX_R8N3 z@zX3`9Ye3)Q2LyubK`Mq47wiSzBOdVu$&fW-Qwx$tCT^0&!bykW!7aKxQJWJJXXq5MqZLy-Mj?qj{Q7`lERb@HRJ-9Q{$imP)s5 z6C4IIq1E*dhuWLyJZU;O^Xkc@Zi#*mldx(WPB`dx|L?-AltnQZJK0&$U6b81&3 zJ35+lA^MusTf@hMP}(4ow2=DXs4!iMJ@(w_s7%5BoKS*Rmk%cK<2|U+RU>UG_NI>8 z?lkt6bksGG!6CgbC+;rlw}l*T4Ra_y4Inl;J&4Eu>Th2)gLz(erOay2v$iDyJN;O9 z{!B>8F?*gWeYJ_MngwlnfRHzXmPzSjiXu0eL*h&kP^ztJWR|i&%EZNQjtSSeWaHoo z#t{j>?RvWaHnR8&OWYefN(1}H?WOs9$eqa4C7bv+;}1=EBN|%-S1Q(}4e8+D!#O$x z#@*HlZFN|+^lckjgqzYzaF>>QS~%X1vGC{lQmix;T~8NCv>GfrZoRi`tYg+cI~A?h zp?tNdm8;xx>Z?HSP<|QFRX6u!B9SxM`3_mm`;iHut@8`;{>y&IQ^CC%ob4NNyi3K; z|6_;@OU9P4EtfBs!FJ$_Gm34Vlq-2nH-;3rE3ZsUJ#{)h+%~i85Hj=uo=EG%Xh zkF|K}R_`yD4}pQCE5~^GQDVMJlknb6qhx-f5D!@FBLPXZK#G=)FRn8v!k-9TM>!`juh(UL{J%Zm~zE z!J~Vt_2e^9VFq;LMAWs|v8RVc&lhYwi>OpKxv$P`>{WX!g{$&&O)%bA`=m}<{;ke$ z#`60qq+OoTZIR1O{aS`%Do&t3-<0%g>T!lD>@(S}gO(A^MIQUJCN>Y~+qH{UVpiTi z4>`Y;en&&NU*HQ<#P_+Lms1#IVLtg%`i#KHi@L62=Xt+DWlKfro5!*aR0T-bV~0

=P;Gr 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

w{vi;}o(vFg#ZZkaHv!#ClrKy6F~rBdLp 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%0Aqm zc+NH30Ln`rY}Z1m7f)41*?4t47lB}-nyfJ)G$T6WPi=48sz_!uu2TPONzvM9$*7B0 zH@0YgxkaG@zizmdk+1Ca4wSLMefZhs;pX}y|E||=Nu+V65_zrZ)A`yDBy(>S1}Tsv zF`8^Q#;>FDpVUr#@80!N5BB0KG=?@h5Ev-Da(yKEk>;n+c3i9v&gec+ACcI^qmhl> zAr@8>EuXbxyczRhl`<~X8>o80A~Ss0&!$_LLFa*yPY(*a-utqj>MMsuaHU4$^BxD2 z3hdcvlJbo1S|i;#&^m(NdDvsFB`kSRU#}@i4wdwB58W2S{zH_$%ps*Y*^kHGa0soo zd#2HZEy}2eG4yM%+cKHzV+Tu4nx89gv>hq~0t}~a*5!Dwce;M_Anq8?71bPmzhzUI z;`b@=C)^~-w@>m{>-LpEcfCN75G7?moTp+_O@ZoCqW|1}b#w~Hlb9*eW_47x;T(hi zh@vU86Oa8(HD3ZcfvnhZm!W@anWip#tSb#=@K7%)cD} zzsOm@%Qy0gJNUtvJk1>R+Uv5`O`Y<_bbV8##Y>!2K&$^C7dvJqTpChcJbIYO%u^7W zx$A%!1=O89*NJe?kC@?^u?4hLk0S;BWGEx}y6t*7(0_8MI$<0%R%aj9q2*)CUIe;F zKD%n{^rxV2T(-W63_fvU#SRn%xis_6az_9C8@YfN7&hTgm%-oMo*6rC{8Dze#0Qrf z*YTph{`cClU0X5e;5vY(exbIRQ+mVtD`4NamO*ZW@v5ggFj5Y4CbJo!2Era@r0)$f>RN2&c z#~{^l%h8emH`+EBV@ael%vF#L(+s+4p{Kgf$5+*g+M6@ZsM|$d-xZoZh0O%(16m-{ zLjO4G)L4->5PcZavhv!{QTC+VAS#_*vx<2ZzIpHAO-pv11k#cZY`cWV&hbyW$cYrvA=#ac_qK{yT>KSp@STx3}I9 zA#2V4Z%^I42IMqKRTs)x5e~tSOp(7#<=+v)Z{OZi-q#in=S?czysN%^hVi2N+2O^8 zaLnKv7|Oexy~LhPjPa8YDw4;)P|}p>qHe)v+R112V`*pQdird#7e+AZVMT-E4^0NlPG0bZc~o^1}~pJ>y&wMK**pPv2G_{V?G z#a}NblaJgOn7`wPTcl7HZxgbiPC&e*xooAGhn6$oz; z1ipfk&DZt_k$yX|$iz3yN6+JB?w`G3h8MSpurY{w4C zS4NjET>E$JX4^6GZ0G-rqWyo#>~7EKUru0qM*msx`hPi$;s3^TH=MMCOm^+qasPB@ zlb3gC&i>2n&)ai~JMU@0kgvBYg*NPcRH$M;vK7Ps{FU?erbL%hU)2D?WUBCepyEV= zwyJSchedule Transfers
- Create a new 7579 compatible Safe Account and use it to schedule + Create a new ERC-7579-compatible Safe Smart Account and use it to schedule transactions.
diff --git a/examples/erc-7579/components/ScheduledTransferForm.tsx b/examples/erc-7579/components/ScheduledTransferForm.tsx index 026ae0a3..3088b961 100644 --- a/examples/erc-7579/components/ScheduledTransferForm.tsx +++ b/examples/erc-7579/components/ScheduledTransferForm.tsx @@ -64,12 +64,13 @@ const ScheduledTransferForm: React.FC<{ safe: SafeSmartAccountClient }> = ({ />
- + setAmount(Number(e.target.value))} value={amount} /> diff --git a/examples/erc-7579/lib/scheduledTransfers.ts b/examples/erc-7579/lib/scheduledTransfers.ts index 666c09b2..1f063aac 100644 --- a/examples/erc-7579/lib/scheduledTransfers.ts +++ b/examples/erc-7579/lib/scheduledTransfers.ts @@ -1,8 +1,11 @@ import { getScheduledTransactionData, + getInstallScheduledTransfersExecutor, getCreateScheduledTransferAction } from '@rhinestone/module-sdk' +import { SafeSmartAccountClient } from './permissionless' + export interface ScheduledTransferDataInput { startDate: number repeatEvery: number @@ -16,7 +19,7 @@ export const scheduledTransfersModuleAddress = const sepoliaUSDCTokenAddress = '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8' export const install7579Module = async ( - safe: any, + safe: SafeSmartAccountClient, scheduledTransferInput: ScheduledTransferDataInput ) => { const { startDate, repeatEvery, numberOfRepeats, amount, recipient } = @@ -33,23 +36,33 @@ export const install7579Module = async ( recipient } - const scheduledTransactionData = getScheduledTransactionData({ + const executionData = getScheduledTransactionData({ scheduledTransaction }) + + const scheduledTransfersModule = getInstallScheduledTransfersExecutor({ + executeInterval: repeatEvery, + numberOfExecutions: numberOfRepeats, + startDate, + executionData + }) + const txHash = await safe.installModule({ type: 'executor', address: scheduledTransfersModuleAddress, - context: scheduledTransactionData + context: scheduledTransfersModule.data as `0x${string}` }) console.log( 'Scheduled transfers module is being installed: https://sepolia.etherscan.io/tx/' + txHash ) + + return txHash } export const scheduleTransfer = async ( - safe: any, + safe: SafeSmartAccountClient, scheduledTransferInput: ScheduledTransferDataInput ) => { const { startDate, repeatEvery, numberOfRepeats, amount, recipient } = diff --git a/examples/passkeys/app/layout.tsx b/examples/passkeys/app/layout.tsx new file mode 100644 index 00000000..9473e493 --- /dev/null +++ b/examples/passkeys/app/layout.tsx @@ -0,0 +1,83 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import Img from 'next/image' +import './globals.css' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'Safe Tutorial: Passkeys', + description: 'Generated by create next app' +} + +export default function RootLayout ({ + children +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + +
+

Passkeys tutorial

+ +
Create a new 4337 compatible Safe Account using passkeys
+
+
+ {children} +
+ + + ) +} diff --git a/examples/passkeys/app/page.tsx b/examples/passkeys/app/page.tsx new file mode 100644 index 00000000..3cd42345 --- /dev/null +++ b/examples/passkeys/app/page.tsx @@ -0,0 +1,149 @@ +'use client' + +import { useState } from 'react' +import { Safe4337Pack } from '@safe-global/relay-kit' +import Img from 'next/image' + +import PasskeyList from '../components/PasskeyList' +import { executeUSDCTransfer } from '../lib/usdc' +import { getPasskeyFromRawId, type PasskeyArgType } from '../lib/passkeys' +import { BUNDLER_URL, CHAIN_NAME, RPC_URL } from '../lib/constants' +import { bufferToString } from '../lib/utils' + +function Create4337SafeAccount () { + const [selectedPasskey, setSelectedPasskey] = useState() + const [safeAddress, setSafeAddress] = useState() + const [isSafeDeployed, setIsSafeDeployed] = useState() + const [userOp, setUserOp] = useState() + + const selectPasskeySigner = async (rawId: string) => { + console.log('selected passkey signer: ', rawId) + + const passkey = await getPasskeyFromRawId(rawId) + + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + rpcUrl: RPC_URL, + signer: passkey, + bundlerUrl: BUNDLER_URL, + options: { + owners: [], + threshold: 1 + } + }) + + const safeAddress = await safe4337Pack.protocolKit.getAddress() + const isSafeDeployed = await safe4337Pack.protocolKit.isSafeDeployed() + + setSelectedPasskey(passkey) + setSafeAddress(safeAddress) + setIsSafeDeployed(isSafeDeployed) + } + + return ( + <> +
+ {selectedPasskey && ( + <> +

Passkey Selected

+ +
+ {bufferToString(selectedPasskey.rawId)} +
+ + )} + +
+ {safeAddress && ( +
+

Safe Account

+ +
+ Address: {safeAddress} +
+
+ Is deployed?:{' '} + {isSafeDeployed ? ( + + Yes{' '} + External link + + ) : ( + 'No' + )} +
+ + {selectedPasskey && ( + + )} + {userOp && isSafeDeployed && ( + <> +
+ Done! Check the transaction status on{' '} + + Jiffy Scan{' '} + External link + +
+ + )} +
+ )} + + ) +} + +export default Create4337SafeAccount diff --git a/examples/passkeys/components/PasskeyList.tsx b/examples/passkeys/components/PasskeyList.tsx new file mode 100644 index 00000000..251eab3e --- /dev/null +++ b/examples/passkeys/components/PasskeyList.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react' + +import { + createPasskey, + loadPasskeysFromLocalStorage, + storePasskeyInLocalStorage, + type PasskeyItemType +} from '../lib/passkeys' + +type Props = { + selectPasskeySigner: (rawId: string) => void +} + +function PasskeyList ({ selectPasskeySigner }: Props) { + const [passkeyList, setPasskeyList] = useState([]) + + async function handleSubmit () { + const passkey = await createPasskey() + storePasskeyInLocalStorage(passkey) + refreshPasskeyList() + } + + function refreshPasskeyList () { + const passkeys = loadPasskeysFromLocalStorage() + setPasskeyList(passkeys) + } + + useEffect(() => { + refreshPasskeyList() + }, []) + + return ( + <> +

Create new Passkey

+ {' '} + {passkeyList.length > 0 && ( + <> +

Passkey List

+ {passkeyList.map(passkey => ( +
+ Id: {passkey.rawId}{' '} + +
+ ))} + + )} + + ) +} + +export default PasskeyList diff --git a/examples/passkeys/lib/constants.ts b/examples/passkeys/lib/constants.ts new file mode 100644 index 00000000..044089a3 --- /dev/null +++ b/examples/passkeys/lib/constants.ts @@ -0,0 +1,7 @@ +export const STORAGE_PASSKEY_LIST_KEY = 'passkeyList' +export const RPC_URL = 'https://ethereum-sepolia-rpc.publicnode.com' +export const CHAIN_NAME = 'sepolia' +export const usdcTokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' // SEPOLIA +export const paymasterAddress = '0x0000000000325602a77416A16136FDafd04b299f' // SEPOLIA +export const BUNDLER_URL = `https://api.pimlico.io/v1/${CHAIN_NAME}/rpc?apikey=${process.env.NEXT_PUBLIC_PIMLICO_API_KEY}` +export const PAYMASTER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${process.env.NEXT_PUBLIC_PIMLICO_API_KEY}` diff --git a/examples/passkeys/lib/passkeys.ts b/examples/passkeys/lib/passkeys.ts new file mode 100644 index 00000000..246cc212 --- /dev/null +++ b/examples/passkeys/lib/passkeys.ts @@ -0,0 +1,137 @@ +import { STORAGE_PASSKEY_LIST_KEY } from './constants' +import { bufferToString, hexStringToUint8Array } from './utils' + +export type PasskeyArgType = { + rawId: ArrayBuffer + publicKey: ArrayBuffer +} +export type PasskeyItemType = { rawId: string; publicKey: string } + +/** + * Create a passkey using WebAuthn API. + * @returns {Promise} Passkey object with rawId and publicKey. + * @throws {Error} If passkey creation fails. + */ +export async function createPasskey (): Promise { + const displayName = 'Safe Owner' // This can be customized to match, for example, a user name. + // Generate a passkey credential using WebAuthn API + const passkeyCredential = await navigator.credentials.create({ + publicKey: { + pubKeyCredParams: [ + { + // ECDSA w/ SHA-256: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 + alg: -7, + type: 'public-key' + } + ], + challenge: crypto.getRandomValues(new Uint8Array(32)), + rp: { + name: 'Safe SmartAccount' + }, + user: { + displayName, + id: crypto.getRandomValues(new Uint8Array(32)), + name: displayName + }, + timeout: 60_000, + attestation: 'none' + } + }) + + if (!passkeyCredential) { + throw Error('Passkey creation failed: No credential was returned.') + } + + console.log('passkeyCredential: ', passkeyCredential) + + const passkey = passkeyCredential as PublicKeyCredential + const attestationResponse = + passkey.response as AuthenticatorAttestationResponse + + const rawId = passkey.rawId + const publicKey = attestationResponse.getPublicKey() + + if (!publicKey) { + throw new Error('getPublicKey error') + } + + return { + rawId, + publicKey + } +} + +/** + * Store passkey in local storage. + * @param {PasskeyArgType} passkey - Passkey object with rawId and publicKey. + */ +export function storePasskeyInLocalStorage (passkey: PasskeyArgType) { + const passkeys = loadPasskeysFromLocalStorage() + + const newPasskeyItem = { + rawId: bufferToString(passkey.rawId), + publicKey: bufferToString(passkey.publicKey) + } + + passkeys.push(newPasskeyItem) + + localStorage.setItem(STORAGE_PASSKEY_LIST_KEY, JSON.stringify(passkeys)) +} + +/** + * Load passkeys from local storage. + * @returns {PasskeyItemType[]} List of passkeys. + */ +export function loadPasskeysFromLocalStorage (): PasskeyItemType[] { + const passkeysStored = localStorage.getItem(STORAGE_PASSKEY_LIST_KEY) + + const passkeyIds = passkeysStored ? JSON.parse(passkeysStored) : [] + + return passkeyIds +} + +/** + * Get public key from local storage. + * @param {string} passkeyRawId - Raw ID of the passkey. + * @returns {ArrayBuffer} Public key. + */ +function getPublicKeyFromLocalStorage (passkeyRawId: string): ArrayBuffer { + const passkeys = loadPasskeysFromLocalStorage() + + const { publicKey } = passkeys.find( + (passkey: PasskeyItemType) => passkey.rawId === passkeyRawId + )! + + return hexStringToUint8Array(publicKey) +} + +/** + * Get passkey from raw ID. + * @param {string} passkeyRawId - Raw ID of the passkey. + * @returns {Promise} Passkey object with rawId and publicKey. + */ +export async function getPasskeyFromRawId ( + passkeyRawId: string +): Promise { + const passkeyCredentials = (await navigator.credentials.get({ + publicKey: { + allowCredentials: [ + { + id: hexStringToUint8Array(passkeyRawId), + type: 'public-key' + } + ], + challenge: crypto.getRandomValues(new Uint8Array(32)), + userVerification: 'required' + } + })) as PublicKeyCredential + + const publicKey = getPublicKeyFromLocalStorage(passkeyRawId) + + const passkey = { + rawId: passkeyCredentials.rawId, + publicKey + } + + return passkey +} diff --git a/examples/passkeys/lib/usdc.ts b/examples/passkeys/lib/usdc.ts new file mode 100644 index 00000000..d87a5cc7 --- /dev/null +++ b/examples/passkeys/lib/usdc.ts @@ -0,0 +1,89 @@ +import { encodeFunctionData, parseAbi } from 'viem' +import { Safe4337Pack } from '@safe-global/relay-kit' + +import { type PasskeyArgType } from './passkeys' +import { + BUNDLER_URL, + CHAIN_NAME, + PAYMASTER_URL, + RPC_URL, + paymasterAddress, + usdcTokenAddress +} from './constants' + +const paymasterOptions = { + isSponsored: true, + paymasterAddress, + paymasterUrl: PAYMASTER_URL +} + +/** + * Generate call data for USDC transfer. + * @param {string} to - Recipient address. + * @param {bigint} value - Amount to transfer. + * @returns {string} Call data. + */ +const generateTransferCallData = (to: string, value: bigint) => { + const abi = parseAbi([ + 'function transfer(address _to, uint256 _value) returns (bool)' + ]) + return encodeFunctionData({ + abi, + functionName: 'transfer', + args: [to as `0x${string}`, value] + }) +} + +/** + * Execute USDC transfer. + * @param {PasskeyArgType} signer - Signer object with rawId and publicKey. + * @param {string} safeAddress - Safe address. + * @returns {Promise} + * @throws {Error} If the operation fails. + */ +export const executeUSDCTransfer = async ({ + signer, + safeAddress +}: { + signer: PasskeyArgType + safeAddress: string +}) => { + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + rpcUrl: RPC_URL, + signer, + bundlerUrl: BUNDLER_URL, + paymasterOptions, + options: { + owners: [ + /* Other owners... */ + ], + threshold: 1 + } + }) + + const usdcAmount = 100_000n // 0.1 USDC + + const transferUSDC = { + to: usdcTokenAddress, + data: generateTransferCallData(safeAddress, usdcAmount), + value: '0' + } + + const safeOperation = await safe4337Pack.createTransaction({ + transactions: [transferUSDC] + }) + + const signedSafeOperation = await safe4337Pack.signSafeOperation( + safeOperation + ) + + console.log('SafeOperation', signedSafeOperation) + + // 4) Execute SafeOperation + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: signedSafeOperation + }) + + return userOperationHash +} diff --git a/examples/passkeys/lib/utils.ts b/examples/passkeys/lib/utils.ts new file mode 100644 index 00000000..2ae879de --- /dev/null +++ b/examples/passkeys/lib/utils.ts @@ -0,0 +1,12 @@ +import { Buffer } from 'buffer' + +export const bufferToString = (buffer: ArrayBuffer): string => + Buffer.from(buffer).toString('hex') + +export function hexStringToUint8Array (hexString: string): Uint8Array { + const arr = [] + for (let i = 0; i < hexString.length; i += 2) { + arr.push(parseInt(hexString.substr(i, 2), 16)) + } + return new Uint8Array(arr) +} diff --git a/package.json b/package.json index ee726397..c10cf07b 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "build": "next build", "dev": "next dev", "generate-api-reference": "node .github/scripts/generateApiReference.js", + "generate-code-examples": "node .github/scripts/generateCodeExamples.js", "generate-supported-networks": "node .github/scripts/generateSupportedNetworks.js", "get-resources-og": "node .github/scripts/getResourcesOg.js", "validate-resources": "node .github/scripts/validateResources.js", diff --git a/pages/advanced/erc-7579/tutorials/7579-tutorial.mdx b/pages/advanced/erc-7579/tutorials/7579-tutorial.mdx index 349e1e3a..8f74fc5e 100644 --- a/pages/advanced/erc-7579/tutorials/7579-tutorial.mdx +++ b/pages/advanced/erc-7579/tutorials/7579-tutorial.mdx @@ -2,6 +2,10 @@ import { Callout } from 'nextra/components' # **How to build an app with Safe and ERC-7579** + + ERC-7579 support is still under development and should not be used in production environments. Please consider this tutorial and all the code examples in it a developer preview. + + The smart account ecosystem was fragmented, with each provider building its own modules often incompatible with other smart account implementations. Developers had to build new modules compatible with their smart accounts or miss out on essential application features. [ERC-7579](https://docs.safe.global/advanced/erc-7579/overview) aims to ensure interoperability across implementations. It defines the account interface so developers can implement modules for all smart accounts that follow this standard. The Safe7579 Adapter makes your Safe compatible with any ERC-7579 modules. As a developer building with Safe, you get access to a rich ecosystem of modules to make your application feature-rich. diff --git a/pages/advanced/smart-account-modules.mdx b/pages/advanced/smart-account-modules.mdx index e96b5345..ac014c96 100644 --- a/pages/advanced/smart-account-modules.mdx +++ b/pages/advanced/smart-account-modules.mdx @@ -8,29 +8,22 @@ Safe Modules add custom features to Safe contracts. They are smart contracts tha Safe Modules can include daily spending allowances, amounts that can be spent without the approval of other owners, recurring transactions modules, and standing orders performed on a recurring date. For example, paying your rent or social recovery modules may allow you to recover a Safe if you lose access to owner accounts. - - Safe Modules can be a security risk since they can execute arbitrary - transactions. Only add trusted and audited modules to a Safe. A malicious - module can take over a Safe. - - ![diagram-safe-modules](../../assets/diagram-safe-modules.png) -## Examples - -### Official Safe Modules - -These are audited Safe Modules built and maintained by the Safe team. The [safe-modules](https://github.com/safe-global/safe-modules) repository contains the collection of the Official Safe Modules. Currently, there are three official Safe Modules: -- 4337 module -- Allowance module -- Passkeys module - -### Community Safe Modules - -1. [Zodiac-compliant modules](https://zodiac.wiki/index.php%3Ftitle=Introduction:_Zodiac_Protocol.html#Modules) - ## How to create a Safe Module A great way to understand how Safe Modules work is by creating one. An excellent place to start is [Safe Modding 101: Create your own Safe Module](https://www.youTube.com/watch?v=nmDYc9PlAic). + +## Examples + +1. [Safe Modules](https://github.com/safe-global/safe-modules) +2. [Zodiac-compliant modules](https://zodiac.wiki/index.php%3Ftitle=Introduction:_Zodiac_Protocol.html#Modules) +3. [Pimlico](https://docs.pimlico.io/permissionless/how-to/accounts/use-safe-account) + + + Safe Modules can be a security risk since they can execute arbitrary + transactions. Only add trusted and audited modules to a Safe. A malicious + module can take over a Safe. + diff --git a/pages/home/4337-guides/permissionless-detailed.mdx b/pages/home/4337-guides/permissionless-detailed.mdx index 9fdfb49e..e1b322a2 100644 --- a/pages/home/4337-guides/permissionless-detailed.mdx +++ b/pages/home/4337-guides/permissionless-detailed.mdx @@ -19,10 +19,14 @@ This guide focuses on how user operations are built and what happens under the h Install [viem](https://npmjs.com/viem) and [permissionless](https://npmjs.com/permissionless) dependencies by running the following command: +{/* */} + ```bash pnpm install viem permissionless ``` +{/* */} + ## Steps diff --git a/pages/home/_meta.json b/pages/home/_meta.json index 355e4ccd..39770c6b 100644 --- a/pages/home/_meta.json +++ b/pages/home/_meta.json @@ -12,5 +12,20 @@ "4337-overview": "Overview", "4337-safe": "Safe and ERC-4337", "4337-supported-networks": "Supported Networks", - "4337-guides": "Guides" + "4337-guides": "Guides", + "-- Passkeys": { + "type": "separator", + "title": "Passkeys" + }, + "passkeys-overview": "Overview", + "passkeys-safe": "Safe and Passkeys", + "passkeys-supported-networks": "Supported Networks", + "passkeys-guides": "Guides", + "passkeys-tutorials": "Tutorials", + "passkeys-faqs": { + "title": "FAQs", + "theme": { + "toc": false + } + } } diff --git a/pages/home/passkeys-faqs.mdx b/pages/home/passkeys-faqs.mdx new file mode 100644 index 00000000..2209efe4 --- /dev/null +++ b/pages/home/passkeys-faqs.mdx @@ -0,0 +1,26 @@ +# Passkeys FAQs + +## Which devices support passkeys? + +Apple and Android devices both support passkeys and syncing. If a device uses [Cross-device authentication (CDA)](https://passkeys.dev/docs/reference/terms/#cross-device-authentication-cda), its passkeys will be portable to other devices. You can read more about device support [here](https://passkeys.dev/device-support/#matrix). + +## How can I sync a passkey across devices? + +Passkeys can be synced across devices through secure cloud services provided by device manufacturers and operating system vendors. Most platforms support passkey syncing natively and automatically, meaning that a passkey used to authenticate a user on one device will also be available on this user’s other devices, for example, via Apple ID or Google Account. + +Only device-bound passkeys (a specific type of passkeys) created on Windows will be available solely on the device they were created on. + +## How can I recover an account with passkeys? + +Account recovery with passkeys typically involves using your synced devices or recovery methods provided by the cloud service where the passkeys are stored. The device manufacturer usually does this automatically, but services like password managers can also be used to store and access passkeys securely, independently from the manufacturer. + +## Do passkeys and web3 use the same encryption schemes? + +Passkeys and web3 share common principles of encryption but may use different schemes depending on the specific implementation. Generally, passkeys use public key cryptography, which is also a foundational element in many web3 protocols: + +- Ethereum primarily uses the `secp256k1` elliptic curve to generate public-private key pairs. This curve was chosen for its properties that offer a good balance of security and performance in blockchain applications. +- Passkeys can support `secp256r1` (also known as `P-256`). `P-256` is commonly used for its strong security properties and compatibility with web technologies. + +## Can I use passkeys with ERC-4337? + +Passkeys can be integrated with ERC-4337, providing enhanced security and user experience in managing web3 accounts. See [our tutorial to build your own implementation](https://docs.safe.global/home/passkeys-tutorials/safe-passkeys-tutorial), or check out [4337 support contract for passkeys](https://github.com/safe-global/safe-modules/tree/main/modules/passkey/contracts/4337) for more information. \ No newline at end of file diff --git a/pages/home/passkeys-guides/_meta.json b/pages/home/passkeys-guides/_meta.json new file mode 100644 index 00000000..27c0385b --- /dev/null +++ b/pages/home/passkeys-guides/_meta.json @@ -0,0 +1,3 @@ +{ + "safe-sdk": "Passkeys with the Safe{Core} SDK" +} diff --git a/pages/home/passkeys-guides/safe-sdk.mdx b/pages/home/passkeys-guides/safe-sdk.mdx new file mode 100644 index 00000000..bca9906b --- /dev/null +++ b/pages/home/passkeys-guides/safe-sdk.mdx @@ -0,0 +1,225 @@ +import { Steps, Tabs } from 'nextra/components' + +# Passkeys with the Safe\{Core\} SDK + +This guide will teach you how to create and execute multiple Safe transactions grouped in a batch from a Safe Smart Account that uses a passkey as an owner. To have a good user experience, we will use an [ERC-4337 compatible Safe](../4337-guides/safe-sdk.mdx) with sponsored transactions using Pimlico infrastructure. During this guide, we will create a new passkey, add it to the Safe as an owner, and use it to sign the user operations. + +This guide uses [Pimlico](https://pimlico.io) as the service provider, but any other provider compatible with the ERC-4337 can be used. + +## Prerequisites + +- [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). +- A [Pimlico account](https://dashboard.pimlico.io) and an API key. +- Passkeys feature is available only in [secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (HTTPS), in some or all [supporting browsers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API#browser_compatibility). + +## Install dependencies + +{/* */} + +```bash +yarn add @safe-global/relay-kit +yarn add @safe-global/protocol-kit +``` + +{/* */} + +## Steps + + + + ### Imports + + Here are all the necessary imports for the script we implement in this guide. + + {/* */} + + ```typescript + import { Safe4337Pack } from '@safe-global/relay-kit' + import { PasskeyArgType } from '@safe-global/protocol-kit' + ``` + + {/* */} + + ### Create a passkey + + Firstly, we need to generate a passkey credential using the WebAuthn API in a supporting browser environment. + + {/* */} + + ```typescript + const RP_NAME = 'Safe Smart Account' + const USER_DISPLAY_NAME = 'User display name' + const USER_NAME = 'User name' + + const passkeyCredential = await navigator.credentials.create({ + publicKey: { + pubKeyCredParams: [ + { + alg: -7, + type: 'public-key' + } + ], + challenge: crypto.getRandomValues(new Uint8Array(32)), + rp: { + name: RP_NAME + }, + user: { + displayName: USER_DISPLAY_NAME, + id: crypto.getRandomValues(new Uint8Array(32)), + name: USER_NAME + }, + timeout: 60_000, + attestation: 'none', + }, + }) + ``` + + {/* */} + + After generating the `passkeyCredential` object, we need to create a new object with the `PasskeyArgType` type that will contain the `rawId` and the `publicKey` information. + + {/* */} + + ```typescript + const passkeyObject = passkeyCredential as PublicKeyCredential + const attestationResponse = + passkeyObject.response as AuthenticatorAttestationResponse + + const rawId = passkeyObject.rawId + const publicKey = attestationResponse.getPublicKey() + + const passkey: PasskeyArgType = { + rawId, + publicKey + } + ``` + + {/* */} + + At this point, it's critical to securely store the information in the `passkey` object in a persistent service. Losing access to this data will result in the user being unable to access their passkey and, therefore, their Safe Smart Account. + + ### Initialize the Safe4337Pack + + Once the passkey is created and secured, we can use the `Safe4337Pack` class exported from the Relay Kit to create, sign, and submit Safe user operations. + + To instantiate this class, the static `init()` method allows connecting existing Safe accounts (as long as they have the `Safe4337Module` enabled) or setting a custom configuration to deploy a new Safe account at the time where the first Safe transaction is submitted. For this guide, we will deploy a new Safe account, configure the paymaster options to get all the transactions sponsored and connect our passkey to add it as the only owner. + + {/* */} + + ```typescript + const PIMLICO_API_KEY = // ... + const RPC_URL = 'https://rpc.ankr.com/eth_sepolia' + + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + rpcUrl: RPC_URL, + signer: passkey, + bundlerUrl: `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_KEY}`, + paymasterOptions: { + isSponsored: true, + paymasterUrl: `https://api.pimlico.io/v2/sepolia/rpc?apikey=${PIMLICO_API_KEY}`, + paymasterAddress: '0x...', + paymasterTokenAddress: '0x...', + sponsorshipPolicyId // Optional value to set the sponsorship policy id from Pimlico + }, + options: { + owners: [], + threshold: 1 + } + }) + ``` + + {/* */} + + ### Create a user operation + + To create a Safe user operation, use the `createTransaction()` method, which takes the array of transactions to execute and returns a `SafeOperation` object. + + {/* */} + + ```typescript + // Define the transactions to execute + const transaction1 = { to, data, value } + const transaction2 = { to, data, value } + + // Build the transaction array + const transactions = [transaction1, transaction2] + + // Create the SafeOperation with all the transactions + const safeOperation = await safe4337Pack.createTransaction({ transactions }) + ``` + + {/* */} + + The `safeOperation` object has the `data` and `signatures` properties, which contain all the information about the transaction batch and the signatures of the Safe owners, respectively. + + ### Sign a user operation + + Before sending the user operation to the bundler, the `safeOperation` object must be signed with the connected passkey. The user is now requested to authenticate with the associated device and sign in with a biometric sensor, PIN, or gesture. + + The `signSafeOperation()` method, which receives a `SafeOperation` object, generates a signature that will be checked when the `Safe4337Module` validates the user operation. + + {/* */} + + ```typescript + const signedSafeOperation = await safe4337Pack.signSafeOperation( + safeTransaction + ) + ``` + + {/* */} + + ### Submit the user operation + + Once the `safeOperation` object is signed with the passkey, we can call the `executeTransaction()` method to submit the user operation to the bundler. + + {/* */} + + ```typescript + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: signedSafeOperation + }) + ``` + + {/* */} + + ### Check the transaction status + + To check the transaction status, we can use the `getTransactionReceipt()` method, which returns the transaction receipt after it's executed. + + {/* */} + + ```typescript + let userOperationReceipt = null + + while (!userOperationReceipt) { + // Wait 2 seconds before checking the status again + await new Promise((resolve) => setTimeout(resolve, 2000)) + userOperationReceipt = await safe4337Pack.getUserOperationReceipt( + userOperationHash + ) + } + ``` + + {/* */} + + In addition, we can use the `getUserOperationByHash()` method with the returned hash to retrieve the user operation object we sent to the bundler. + + {/* */} + + ```typescript + const userOperationPayload = await safe4337Pack.getUserOperationByHash( + userOperationHash + ) + ``` + + {/* */} + + + +## Recap and further reading + +After following this guide, we are able to deploy a new ERC-4337 compatible Safe Smart Account setup with a passkey and create, sign, and execute Safe transactions signing them with the passkey. Learn more about passkeys and how Safe supports them in detail by following these links: + +- [Safe\{Core\} SDK demo](https://github.com/5afe/passkey-sdk-demo-dapp) +- [Safe Passkeys contracts](https://github.com/safe-global/safe-modules/tree/main/modules/passkey) diff --git a/pages/home/passkeys-overview.mdx b/pages/home/passkeys-overview.mdx new file mode 100644 index 00000000..d693f966 --- /dev/null +++ b/pages/home/passkeys-overview.mdx @@ -0,0 +1,53 @@ +import { Grid } from '@mui/material' +import CustomCard from '../../components/CustomCard' + +# What are passkeys? + +Passkeys are a standard authentication method designed to avoid using traditional passwords, providing a more secure and user-friendly experience. + +Passkeys are based on public and private key pairs to secure user authentication. The public key is stored on the server side, while the private key is secured in the user's device. The user is authenticated by proving ownership of the private key, usually with biometric sensors, without extracting it from the device at any time. This method ensures that sensitive information remains protected and reduces the risk of credential theft. + +## Why do we need passkeys? + +Passkeys offer significant security improvements over traditional passwords. In the context of web3, where secure key management is paramount, passkeys provide an efficient alternative to seed phrases, which are often considered both a security liability and a subpar user experience. + + + + + + + + + + + + + +Safe offers the capability to sign into your wallet using passkeys by implementing a dedicated module that verifies the integrity of the key provided. + +## Further reading + +- [The official W3C standard](https://www.w3.org/TR/webauthn) +- [WebAuthn API specification](https://webauthn.wtf/how-it-works/basics) +- [Passkeys 101 by FIDO Alliance](https://fidoalliance.org/passkeys) diff --git a/pages/home/passkeys-safe.mdx b/pages/home/passkeys-safe.mdx new file mode 100644 index 00000000..e8ec63df --- /dev/null +++ b/pages/home/passkeys-safe.mdx @@ -0,0 +1,63 @@ +import { Callout } from 'nextra/components' + +# Safe and Passkeys + + + Passkeys are compatible with Safe versions `≥1.3.0`. + + +Safe's standard-agnostic nature allows adding or removing user flows, such as custom signature verification logic. This flexibility facilitates the integration of a Passkeys-based execution flow into a Safe. Safe passkey contracts conform to both ERC-1271 and WebAuthn standards, enabling the verification of signatures for WebAuthn credentials that use the `secp256r1` curve. + +These contracts can utilize EIP-7212 precompiles for signature verification on supported networks or alternatively employ any verifier contract as a fallback mechanism. + +## Passkey contracts + + + This section covers implementation details of the passkeys with Safe. If you'd rather get straight to building, head over to our [guides](./passkeys-guides/safe-sdk) and [tutorials](./passkeys-tutorials/safe-passkeys-tutorial) sections. + + +### `SafeWebAuthnSignerProxy` + +A proxy contract is uniquely deployed for each `Passkey` signer. The signer information, such as Public key coordinates, Verifier address, and Singleton address, is immutable. All calls to the signer are forwarded to the `SafeWebAuthnSignerSingleton` contract. + +`SafeWebAuthnSignerProxy` provides gas savings compared to the whole contract deployment for each signer creation. + +`SafeWebAuthnSignerProxy` and `SafeWebAuthnSignerSingleton` use no storage slots to avoid storage access violations defined in ERC-4337. Check [this PR](https://github.com/safe-global/safe-modules/pull/370) for details on gas savings. This non-standard proxy contract appends signer information, like public key coordinates and verifier data, to the call data before forwarding the calls to the singleton contract. + +### `SafeWebAuthnSignerSingleton` + +`SafeWebAuthnSignerSingleton` is a singleton contract that implements the ERC-1271 interface to support signature verification. It enables signature data to be forwarded from a Safe to the `WebAuthn` library. This contract expects the caller to append public key coordinates and the verifier address (inspired by [ERC-2771](https://eips.ethereum.org/EIPS/eip-2771)). + +### `SafeWebAuthnSignerFactory` + +The `SafeWebAuthnSignerFactory` contract deploys the `SafeWebAuthnSignerProxy` contract with the public key coordinates and verifier information. The factory contract also supports signature verification for the public key and signature information without deploying the signer contract, which is used during the validation of ERC-4337 user operations by the experimental `SafeSignerLaunchpad` contract. + +New signers can be deployed using the [ISafeSignerFactory](https://github.com/safe-global/safe-modules/blob/466a9b8ef169003c5df856c6ecd295e6ecb9e99d/modules/passkey/contracts/interfaces/ISafeSignerFactory.sol) interface and this factory contract address. + +### `WebAuthn` + +This library generates a signing message, hashing it, and forwards the call to the verifier contract. The `WebAuthn` library defines a `Signature` struct containing `authenticatorData` and `clientDataFields`, followed by the ECDSA signature's `r` and `s` components. + +The `authenticatorData` and `clientDataFields` are required for generating the signing message. The `bytes` signature received in the `verifySignature(...)` function is cast to the `Signature` struct, so the caller has to take into account formatting the signature bytes as expected by the `WebAuthn` library. +The code snippet below shows signature encoding for verification using the WebAuthn library. + +``` +bytes authenticatorData = ...; +string clientDataFields = ...; +uint256 r = ...; +uint256 s = ...; +// Encode the signature data +bytes memory signature = abi.encode(authenticatorData, clientDataFields, r, s); +``` + +### `P256` + +`P256` is a library for P256 signature verification with contracts that follow the EIP-7212 EC verify precompile interface. This library defines a custom type `Verifiers`, which encodes two addresses into a single `uint176`. The first address (2 bytes) is a precompile address dedicated to verification, and the second (20 bytes) is a fallback address. + +This setup allows the library to support networks where the precompile is not yet available. It seamlessly transitions to the precompile when it becomes active while relying on a fallback contract address in the meantime. + +## Further reading + +- [Passkeys module](https://github.com/safe-global/safe-modules/blob/466a9b8ef169003c5df856c6ecd295e6ecb9e99d/modules/passkey/README.md) +- [Safe and Passkeys demo application](https://github.com/safe-global/safe-modules/tree/main/examples/4337-passkeys) +- [4337 support for passkeys](https://github.com/safe-global/safe-modules/tree/main/modules/passkey/contracts/4337) \ No newline at end of file diff --git a/pages/home/passkeys-supported-networks.mdx b/pages/home/passkeys-supported-networks.mdx new file mode 100644 index 00000000..e16b8f96 --- /dev/null +++ b/pages/home/passkeys-supported-networks.mdx @@ -0,0 +1,19 @@ +# Supported Networks + +The Safe Passkeys Module `v0.2.0` is deployed in the following networks: + +| Network | `SafeWebAuthnSignerFactory` Address | `DaimoP256Verifier` Address | `FCLP256Verifier` Address | +| ---------------------------- | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | +| Arbitrum | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | +| Arbitrum Sepolia | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | +| Base | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | +| Base Sepolia | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | +| Ethereum Mainnet | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | +| Ethereum Sepolia | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | +| Muster | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | +| Optimism | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | +| Optimism Sepolia | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | +| Polygon | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | +| Polygon Mumbai | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | + +To add additional deployments please follow the [deployment instructions](https://github.com/safe-global/safe-modules/tree/main/modules/passkey#deploy) in the Safe Modules repository. diff --git a/pages/home/passkeys-tutorials/_meta.json b/pages/home/passkeys-tutorials/_meta.json new file mode 100644 index 00000000..3f800121 --- /dev/null +++ b/pages/home/passkeys-tutorials/_meta.json @@ -0,0 +1,4 @@ +{ + "safe-passkeys-tutorial": "Build an app with Safe and passkeys" +} + \ No newline at end of file diff --git a/pages/home/passkeys-tutorials/safe-passkeys-tutorial.mdx b/pages/home/passkeys-tutorials/safe-passkeys-tutorial.mdx new file mode 100644 index 00000000..f6487b61 --- /dev/null +++ b/pages/home/passkeys-tutorials/safe-passkeys-tutorial.mdx @@ -0,0 +1,233 @@ +import { Callout } from 'nextra/components' + +# How to build an app with Safe and passkeys + + + Passkeys support is still under audit and should not be used in production environments. Please consider this tutorial and all the code examples in it a developer preview. + + +An increasing number of applications rely on passkeys to authenticate users securely and with little friction. Security and user-friendliness are crucial to making web3 a reality for the next billion users. +Being able to unlock a Safe Smart Account with your fingerprints or Face ID, sending transactions without worrying about third-party wallet interfaces, phishing attempts, or securing seed phrases will bring new forms of ownership to the connected world. +Today, we'll learn how to make this a reality using [Safe\{Core\} SDK](../../sdk/overview.mdx), [Pimlico](https://www.pimlico.io/), and [Next.js](https://nextjs.org/docs). + +This tutorial will demonstrate creating a web app for using [passkeys](../passkeys-overview.mdx) in your Safe. This app will allow you to: +- Create a new passkey secured by the user's device. +- Deploy a new Safe on Ethereum Sepolia for free. +- Sign a transaction to send USDC using the previously created passkey. + +![safe-passkeys-app-2.png](../../../assets/safe-passkeys-app-2.png) + +## **What you'll need** + +**Prerequisite knowledge:** You will need some basic experience with [React](https://react.dev/learn), Next.js, and [ERC-4337](../4337-overview). + +Before progressing with the tutorial, please make sure you have: + +- Downloaded and installed [Node.js](https://nodejs.org/en/download/package-manager) and [pnpm](https://pnpm.io/installation). +- Created an API key from [Pimlico](https://www.pimlico.io/). + +**Note:** If you wish to follow along using the completed project, you can [check out the GitHub repository](https://github.com/5afe/safe-passkeys-tutorial) for this tutorial. + +## 1. Setup a Next.js application + +Initialize a new Next.js app using pnpm with the following command: + +```bash +pnpm create next-app +``` + +When prompted by the CLI: + +- Select `yes` to TypeScript, ESLint, and App router. +- Select `no` to all other questions (Tailwind, `src` directory, and import aliases). + +### Install dependencies + +For this project, we'll use the [Relay Kit from the Safe\{Core\} SDK](../../sdk/relay-kit.mdx) to set up a Safe, sponsor a USDC transaction, and use [viem](https://www.npmjs.com/package/viem) and [buffer](https://www.npmjs.com/package/buffer) for some helper functions. + +Run the following command to add all these dependencies to the project: + +```bash +pnpm add @safe-global/relay-kit@3.1.0-alpha.0 buffer viem +``` + +Now, create a file named `.env.local` at the root of your project, and add your Pimlico API key to it: + +```bash +echo "NEXT_PUBLIC_PIMLICO_API_KEY='your_pimlico_api_key_goes_here'" > .env.local +``` + +### Run the development server + +Run the local development server with the following command: + +```bash +pnpm dev +``` + +Go to `http://localhost:3000` in your browser to see the default Next.js application. + +![next.png](../../../assets/next.png) + +## 2. Add project constants and utilities + +Create a `lib` folder at the project root and add a file `constants.ts` containing common constants used throughout the project: + +```bash +mkdir lib +cd lib +touch constants.ts +``` + +Add the following code to the `constants.ts` file: + +```tsx +// from ../../../examples/passkeys/lib/constants.ts +``` + +In the same `lib` folder, create a `utils.ts` file: + +```bash +touch utils.ts +``` + +Add the following code to the `utils.ts` file: + +```tsx +// from ../../../examples/passkeys/lib/utils.ts +``` + +This file contains two utilities for manipulating passkey objects from the native [`navigator.credentials`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/credentials) API: + +- `bufferToString`: Helps us read the passkeys properties (returned by the browser) as strings. +- `hexStringToUint8Array`: Helps us pass string arguments to the `credentials` API. + +## 3. Add passkeys functionality + +In the `lib` folder, create a file called `passkeys.ts` : + +```bash +touch passkeys.ts +``` + +This file will contain all the logic required to operate passkey: + +- Create and recover them using the user's device. +- Store and retrieve them from/to the local storage. + +**Note:** You can also store the passkeys on a remote database or the user's device. + +```tsx +// from ../../../examples/passkeys/lib/passkeys.ts +``` + +In this file, we have four functions: + +- `createPasskey`, which helps create a new passkey. +- `storePasskeyInLocalStorage`, which helps store it in the browser's local storage. +- `loadPasskeysFromLocalStorage`, which helps load a passkey from local storage. +- `getPublicKeyFromLocalStorage`, which helps find a passkey in the local storage corresponding to a given `rawId` and returns this passkey's public key. +- `getPasskeyFromRawId`, which helps reconstruct a full passkey from a `rawId` and a public key stored in local storage. + +## 4. Add USDC transaction functionality + +Create a `usdc.ts` file in the `lib` folder to add functions to prepare and send a transaction transferring USDC from our yet-to-come Safe. + +```tsx +touch usdc.ts +``` + +Add the following code to the `usdc.ts` file: + +```tsx +// from ../../../examples/passkeys/lib/usdc.ts +``` + +With this configuration, a new Safe will be created (but not yet deployed) when a passkey is selected. This Safe will be deployed when its first transaction is executed. + +**Note:** Transferring USDC was chosen here just as an example, and any other transaction would have the same effect. + +## 5. Add UI components + +Let's add a user interface to create and store a passkey on the user's device, deploy a safe, and sign the USDC transaction. + +Create a `components` folder at the project root with a file named `PasskeyList.tsx`: + +```bash +cd .. +mkdir components +cd components +touch PasskeyList.tsx +``` + +Add the following code to the `PasskeyList.tsx` file: + +```tsx +// from ../../../examples/passkeys/components/PasskeyList.tsx +``` + +This component displays a list of previously created passkeys and a button for creating new ones. + +Lastly, replace the content of the `page.tsx` file, within the `app` folder, with this code: + +```tsx +// from ../../../examples/passkeys/app/page.tsx +``` + +This UI will put everything we built in the previous steps into a coherent application with all the functionality required to let you create a passkey, select it, and use it to sign a transaction. + +## 6. (Optional) Add styling to the app + +Because a web app is nothing without good styling, let's add some Safe design to our project 💅. + +Still within the `app` folder, replace the existing content of the file `layout.tsx` with this code: + +```tsx +// from ../../../examples/passkeys/app/layout.tsx +``` + +In the same folder, add some margin to the titles, by adding this code at the end of the `globals.css` file: + +```css +h1, +h2, +h3 { + margin-top: 40px; + margin-bottom: 10px; +} + +button { + cursor: pointer; + border: none; + background: #00E673; + color: black; + padding: 10px 20px; + border-radius: 5px; + margin: 10px 0; +} +``` + +Finally, in the `public` folder, add these three icons. You can find them in the project's GitHub repository: [`safe.svg`](https://github.com/5afe/safe-passkeys-tutorial/blob/main/public/safe.svg), [`github.svg`](https://github.com/5afe/safe-passkeys-tutorial/blob/main/public/github.svg/), and [`external-link.svg`](https://github.com/5afe/safe-passkeys-tutorial/blob/main/public/external-link.svg). + +## Testing your Safe passkeys app + +That's it! You can find the source code for the example created in this tutorial [on GitHub](https://github.com/5afe/safe-passkeys-tutorial). You can now return to your browser and see the app displayed 🎉. + +![safe-passkeys-app-1.png](../../../assets/safe-passkeys-app-1.png) + +Click the **Add New Passkey** button to prompt a browser pop-up asking you to confirm the creation of a new passkey. This passkey will be stored in your browser's local storage and displayed in the list above the button. + +Once confirmed, select this passkey by clicking **Select** next to it. This will prompt another pop-up window, this time asking to confirm the use of the previously created passkey. + +![safe-passkeys-app-2.png](../../../assets/safe-passkeys-app-2.png) + +At this stage, the app will have created a safe object awaiting deployment. Send some USDC to your future safe by clicking the link to [Circle's USDC faucet](https://faucet.circle.com/) for Sepolia and entering the Safe's address. By clicking **Sign transaction with passkey**, the deployment of this safe will then be embedded in a batch transaction, along with the transfer of USDC. +Open the console to see the UserOp that was sent or click the link provided to Jiffy scan for more complete information. + +## Do more with Safe and passkeys + +Today, we learned how to use passkeys (create them, store them, and use them securely) and how they can interact with a Safe (deploy it and send transactions). We hope you enjoyed this tutorial and that the combination of passkeys and 4337 will unlock new forms of ownership for your project and users. + +You can now integrate passkeys with more transactions and functionalities of the Safe ecosystem. You can read more about passkeys in our [overview](../passkeys-overview) or in the [WebAuthn API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API). + +Did you encounter any difficulties? Let us know by opening [an issue](https://github.com/5afe/safe-passkeys-tutorial/issues/new) or asking a question on [Stack Exchange](https://ethereum.stackexchange.com/questions/tagged/safe-core) with the `safe-core` tag. diff --git a/pages/sdk/protocol-kit/reference/safe-factory.md b/pages/sdk/protocol-kit/reference/safe-factory.md index a344e889..bb11b961 100644 --- a/pages/sdk/protocol-kit/reference/safe-factory.md +++ b/pages/sdk/protocol-kit/reference/safe-factory.md @@ -15,6 +15,25 @@ const safeFactory = await SafeFactory.init({ }) ``` +- The `signer` property + + A passkey object can be passed as a signer to initialize an instance of the Safe Factory. + +```typescript +import { SafeFactory, PasskeyArgType } from '@safe-global/protocol-kit' + +const passkey: PasskeyArgType = { + rawId, + publicKey, +} + +const safeFactory = await SafeFactory.init({ + provider, + signer: passkey +}) +``` + + - The `isL1SafeSingleton` flag Two versions of the Safe contracts are available: [Safe.sol](https://github.com/safe-global/safe-contracts/blob/v1.4.1/contracts/Safe.sol) that doesn't trigger events to save gas and [SafeL2.sol](https://github.com/safe-global/safe-contracts/blob/v1.4.1/contracts/SafeL2.sol) that does, which is more appropriate for L2 networks. @@ -52,6 +71,7 @@ const safeFactory = await SafeFactory.init({ signMessageLibAddress: '', createCallAddress: '', simulateTxAccessorAddress: '', + safeWebAuthnSignerFactoryAddress:'', safeSingletonAbi: '', // Optional. Only needed with web3.js safeProxyFactoryAbi: '', // Optional. Only needed with web3.js multiSendAbi: '', // Optional. Only needed with web3.js @@ -60,6 +80,7 @@ const safeFactory = await SafeFactory.init({ signMessageLibAbi: '', // Optional. Only needed with web3.js createCallAbi: '', // Optional. Only needed with web3.js simulateTxAccessorAbi: '' // Optional. Only needed with web3.js + safeWebAuthnSignerFactoryAbi: '' // Optional. Only needed with web3.js } } diff --git a/pages/sdk/protocol-kit/reference/safe.md b/pages/sdk/protocol-kit/reference/safe.md index 69e55b5c..9a088bf4 100644 --- a/pages/sdk/protocol-kit/reference/safe.md +++ b/pages/sdk/protocol-kit/reference/safe.md @@ -37,6 +37,26 @@ const protocolKit = await Safe.init({ }) ``` +- The `signer` property + + A passkey object can be passed as a signer to initialize an instance of the Protocol Kit. + +```typescript +import Safe from '@safe-global/protocol-kit' + +const passkey: PasskeyArgType = { + rawId, + publicKey, +} + +const protocolKit = await Safe.init({ + provider, + signer: passkey, + // safeAddress or predictedSafe +}) +``` + + - The `isL1SafeSingleton` flag Two versions of the Safe contracts are available: [Safe.sol](https://github.com/safe-global/safe-contracts/blob/v1.4.1/contracts/Safe.sol) that doesn't trigger events to save gas and [SafeL2.sol](https://github.com/safe-global/safe-contracts/blob/v1.4.1/contracts/SafeL2.sol) that does, which is more appropriate for L2 networks. @@ -75,6 +95,7 @@ const protocolKit = await Safe.init({ signMessageLibAddress: '', createCallAddress: '', simulateTxAccessorAddress: '', + safeWebAuthnSignerFactoryAddress:'', safeSingletonAbi: '', // Optional. Only needed with web3.js safeProxyFactoryAbi: '', // Optional. Only needed with web3.js multiSendAbi: '', // Optional. Only needed with web3.js @@ -83,6 +104,7 @@ const protocolKit = await Safe.init({ signMessageLibAbi: '', // Optional. Only needed with web3.js createCallAbi: '', // Optional. Only needed with web3.js simulateTxAccessorAbi: '' // Optional. Only needed with web3.js + safeWebAuthnSignerFactoryAbi: '' // Optional. Only needed with web3.js } } @@ -174,6 +196,7 @@ protocolKit = await protocolKit.connect({ predictedSafe }) signMessageLibAddress: '', createCallAddress: '', simulateTxAccessorAddress: '', + safeWebAuthnSignerFactoryAddress:'', safeSingletonAbi: '', // Optional. Only needed with web3.js safeProxyFactoryAbi: '', // Optional. Only needed with web3.js multiSendAbi: '', // Optional. Only needed with web3.js @@ -182,6 +205,7 @@ protocolKit = await protocolKit.connect({ predictedSafe }) signMessageLibAbi: '', // Optional. Only needed with web3.js createCallAbi: '', // Optional. Only needed with web3.js simulateTxAccessorAbi: '' // Optional. Only needed with web3.js + safeWebAuthnSignerFactoryAbi: '' // Optional. Only needed with web3.js } } @@ -513,6 +537,22 @@ const txResponse = await protocolKit.executeTransaction(safeTransaction) await txResponse.transactionResponse?.wait() ``` +Instead of using an address, this method also supports the use of a passkey to set the address of the new owner: + +```typescript +const passkey: PasskeyArgType = { + rawId, + publicKey, +} +const params: AddPasskeyOwnerTxParams = { + passkey, + threshold // Optional. If `threshold` isn't provided the current threshold won't change. +} +const safeTransaction = await protocolKit.createAddOwnerTx(params) +const txResponse = await protocolKit.executeTransaction(safeTransaction) +await txResponse.transactionResponse?.wait() +``` + This method can optionally receive the `options` parameter: ```typescript @@ -534,6 +574,22 @@ const txResponse = await protocolKit.executeTransaction(safeTransaction) await txResponse.transactionResponse?.wait() ``` +Instead of using an address, this method also supports the use of a passkey to remove an owner: + +```typescript +const passkey: PasskeyArgType = { + rawId, + publicKey, +} +const params: AddPasskeyOwnerTxParams = { + passkey, + threshold // Optional. If `newThreshold` isn't provided, the current threshold will be decreased by one. +} +const safeTransaction = await protocolKit.createRemoveOwnerTx(params) +const txResponse = await protocolKit.executeTransaction(safeTransaction) +await txResponse.transactionResponse?.wait() +``` + This method can optionally receive the `options` parameter: ```typescript @@ -555,6 +611,22 @@ const txResponse = await protocolKit.executeTransaction(safeTransaction) await txResponse.transactionResponse?.wait() ``` +Instead of using an address, this method also supports any combination of passkey and address: + +```typescript +const newOwnerPasskey: PasskeyArgType = { + rawId, + publicKey, +} +const params: SwapOwnerTxParams = { + oldOwnerAddress, + newOwnerPasskey +} +const safeTransaction = await protocolKit.createSwapOwnerTx(params) +const txResponse = await protocolKit.executeTransaction(safeTransaction) +await txResponse.transactionResponse?.wait() +``` + This method can optionally receive the `options` parameter: ```typescript @@ -591,6 +663,17 @@ Checks if a specific address is an owner of the current Safe. const isOwner = await protocolKit.isOwner(address) ``` +A passkey can also be used to check if the signer account is an owner of the current Safe. + +```typescript +const passkey: PasskeyArgType = { + rawId, + publicKey, +} + +const isOwner = await protocolKit.isOwner(passkey) +``` + ## Threshold ### `createChangeThresholdTx` diff --git a/pages/sdk/relay-kit/reference/safe-4337-pack.mdx b/pages/sdk/relay-kit/reference/safe-4337-pack.mdx index 25f3bb69..5e5dcc02 100644 --- a/pages/sdk/relay-kit/reference/safe-4337-pack.mdx +++ b/pages/sdk/relay-kit/reference/safe-4337-pack.mdx @@ -39,7 +39,7 @@ The `Safe4337InitOptions` used in the `init()` method are: ```typescript Safe4337InitOptions = { provider: Eip1193Provider | HttpTransport | SocketTransport - signer?: HexAddress | PrivateKey + signer?: HexAddress | PrivateKey | PasskeyArgType bundlerUrl: string safeModulesVersion?: string customContracts?: { @@ -87,7 +87,8 @@ PaymasterOptions = { ``` - **`provider`** : The EIP-1193 compatible provider or RPC URL of the selected chain. -- **`signer`** : The signer private address if the `provider` doesn't resolve to a signer account. If the `provider` resolves to multiple signer addresses, the `signer` property can be used to specify which account to connect, otherwise the first address returned will be used. +- **`signer`** : A passkey or the signer private key if the `provider` doesn't resolve to a signer account. If the `provider` resolves to multiple signer addresses, the `signer` property can be used to specify which account to connect, otherwise the first address returned will be used. +- **`rpcUrl`** : The RPC URL of the selected chain. - **`bundlerUrl`** : The bundler's URL. - **`safeModulesVersion`** : The version of the [Safe Modules contract](https://github.com/safe-global/safe-modules-deployments/tree/main/src/assets/safe-4337-module). - **`customContracts`** : An object with custom contract addresses. This is optional, if no custom contracts are provided, default ones will be used. diff --git a/styles/styles.css b/styles/styles.css index 28ca2276..0ec2b65c 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -15,10 +15,6 @@ h2 { line-height: 42px; } -h3 { - line-height: 36px; -} - /* Navbar */ #__next > div > div > div { backdrop-filter: blur(8px);