From fa33d7e81e66f81d8e536d59dd473fdc3c771eeb Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Tue, 16 Jul 2024 09:55:42 +0100 Subject: [PATCH] feat: session_dx --- .env.example | 1 + .github/workflows/build.yml | 2 + .github/workflows/coverage.yml | 2 + .github/workflows/docs.yml | 2 + .github/workflows/pr-lint.yml | 2 + .github/workflows/size-report.yml | 10 +- .github/workflows/test-read.yml | 2 + .github/workflows/test-write.yml | 6 + .size-limit.json | 8 +- CHANGELOG.md | 6 + biome.json | 7 +- bun.lockb | Bin 273887 -> 215441 bytes bunfig.toml | 2 + examples/CREATE_AND_USE_A_BATCH_SESSION.md | 19 +- examples/CREATE_AND_USE_A_SESSION.md | 30 +- package.json | 11 +- src/account/BiconomySmartAccountV2.ts | 1262 +++++++++-------- src/account/utils/Constants.ts | 7 +- src/account/utils/Types.ts | 440 +++--- src/account/utils/convertSigner.ts | 4 +- src/account/utils/getChain.ts | 1 + src/bundler/utils/Constants.ts | 2 - src/bundler/utils/getAAError.ts | 23 +- src/modules/index.ts | 7 +- src/modules/interfaces/ISessionStorage.ts | 3 +- .../session-storage/SessionLocalStorage.ts | 4 +- .../session-storage/SessionMemoryStorage.ts | 17 - src/modules/sessions/abi.ts | 94 +- src/modules/sessions/dan.ts | 337 +++++ .../sessions/sessionSmartAccountClient.ts | 142 +- src/modules/utils/Constants.ts | 2 + src/modules/utils/Helper.ts | 95 +- src/modules/utils/Types.ts | 31 + .../walletprovider-sdk/EOAauthentication.ts | 73 + .../walletprovider-sdk/authentication.ts | 70 + src/modules/walletprovider-sdk/encoding.ts | 5 + src/modules/walletprovider-sdk/index.ts | 2 + .../walletprovider-sdk/networkSigner.ts | 99 ++ src/modules/walletprovider-sdk/types.ts | 5 + src/modules/walletprovider-sdk/viemSigner.ts | 12 + .../walletProviderServiceClient.ts | 27 + .../walletProviderServiceClientInterface.ts | 35 + tests/globalSetup.ts | 1 + tests/modules/write.test.ts | 1190 ++++++++-------- tests/playground/write.test.ts | 21 +- 45 files changed, 2572 insertions(+), 1549 deletions(-) create mode 100644 bunfig.toml create mode 100644 src/modules/sessions/dan.ts create mode 100644 src/modules/walletprovider-sdk/EOAauthentication.ts create mode 100644 src/modules/walletprovider-sdk/authentication.ts create mode 100644 src/modules/walletprovider-sdk/encoding.ts create mode 100644 src/modules/walletprovider-sdk/index.ts create mode 100644 src/modules/walletprovider-sdk/networkSigner.ts create mode 100644 src/modules/walletprovider-sdk/types.ts create mode 100644 src/modules/walletprovider-sdk/viemSigner.ts create mode 100644 src/modules/walletprovider-sdk/walletProviderServiceClient.ts create mode 100644 src/modules/walletprovider-sdk/walletProviderServiceClientInterface.ts diff --git a/.env.example b/.env.example index d1e8fcfe8..4c5a02a1a 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,4 @@ E2E_BICO_PAYMASTER_KEY_BASE= CHAIN_ID=80002 CODECOV_TOKEN= TESTING=false +SILENCE_LABS_NPM_TOKEN=npm_XXX \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08c18cd34..f264edf53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,3 +12,5 @@ jobs: - name: Build uses: ./.github/actions/build + env: + SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 6b2c9aebc..6ff0f943c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,6 +17,8 @@ jobs: - name: Install dependencies uses: ./.github/actions/install-dependencies + env: + SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Run the tests run: bun run test:coverage diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 078b083ba..63ed3848b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,6 +17,8 @@ jobs: - name: Install dependencies uses: ./.github/actions/install-dependencies + env: + SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Set remote url run: git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/bcnmy/biconomy-client-sdk.git diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 3e77f7837..028022ec8 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -12,6 +12,8 @@ jobs: - name: Install dependencies uses: ./.github/actions/install-dependencies + env: + SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Use commitlint to check PR title run: echo "${{ github.event.pull_request.title }}" | bun commitlint diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml index 68718940e..63e396f34 100644 --- a/.github/workflows/size-report.yml +++ b/.github/workflows/size-report.yml @@ -21,15 +21,15 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - name: Set up Bun uses: oven-sh/setup-bun@v1 - - name: Install dependencies - shell: bash - run: | - bun install --frozen-lockfile + - name: Build + uses: ./.github/actions/build + env: + SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Report bundle size uses: andresz1/size-limit-action@master diff --git a/.github/workflows/test-read.yml b/.github/workflows/test-read.yml index f6338828d..14f88a31d 100644 --- a/.github/workflows/test-read.yml +++ b/.github/workflows/test-read.yml @@ -16,6 +16,8 @@ jobs: - name: Install dependencies uses: ./.github/actions/install-dependencies + env: + SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Run the tests run: bun run test:ci -t=Read diff --git a/.github/workflows/test-write.yml b/.github/workflows/test-write.yml index 33c230deb..446fb63df 100644 --- a/.github/workflows/test-write.yml +++ b/.github/workflows/test-write.yml @@ -16,8 +16,14 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install dependencies uses: ./.github/actions/install-dependencies + env: + SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Run the account tests run: bun run test:ci -t=Account:Write diff --git a/.size-limit.json b/.size-limit.json index aff9966a3..52596234f 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,20 +2,20 @@ { "name": "core (esm)", "path": "./dist/_esm/index.js", - "limit": "65 kB", + "limit": "80 kB", "import": "*", "ignore": ["node:fs", "fs"] }, { "name": "core (cjs)", "path": "./dist/_cjs/index.js", - "limit": "65 kB", + "limit": "80 kB", "ignore": ["node:fs", "fs"] }, { "name": "account (tree-shaking)", "path": "./dist/_esm/index.js", - "limit": "65 kB", + "limit": "80 kB", "import": "{ createSmartAccountClient }", "ignore": ["node:fs", "fs"] }, @@ -36,7 +36,7 @@ { "name": "modules (tree-shaking)", "path": "./dist/_esm/modules/index.js", - "limit": "60 kB", + "limit": "80 kB", "import": "{ createSessionKeyManagerModule }", "ignore": ["node:fs", "fs"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 076b84ef4..8dc4bdced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @biconomy/account +## 4.6.0 + +### Minor Changes + +- Distributed Sessions + ## 4.5.2 ### Patch Changes diff --git a/biome.json b/biome.json index 262693997..4e9fc4e32 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", "files": { "ignore": [ "package.json", @@ -14,7 +14,8 @@ "_types", "bun.lockb", "docs", - "dist" + "dist", + "walletprovider-sdk" ] }, "organizeImports": { @@ -42,7 +43,7 @@ "javascript": { "formatter": { "semicolons": "asNeeded", - "trailingComma": "none" + "enabled": false } } } diff --git a/bun.lockb b/bun.lockb index 04fa9842978369116edd09d1a09b84c4e9d488cb..697228775c662ee4e101aff50d3369180021c157 100755 GIT binary patch delta 51071 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 z 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 delta 87521 zcmeFacUTlj)IB^if-uUcU;q>)selRw!~w|$M6!Ye6_qd~$w5KE0Wn~})T<*Z=70gS zA}T6oFlRC6fH@0_{?6_0aoJtp_j}Xx{k8q<%;|Hg>ej7Wp{omU=Q5JJH=E6|Fk7W- zw6tUFm81&eijC7fB&Iulnm1m2tWWK2=cbz#dg3`W&DHs0nNd?Aw6Lmj`TIFI~#(oq&{Ii2TifM^P);+XZY2{6zJb z<7Sd7m*EzW5<=rGVp8BwH8POll?^$(CuCX++sWrZ7DJv0YyuoZb_klrEjn{{OXoANkq$SEwZ524>@4~0Yz&P=C;HXcY-4BMz z0jiL~rmXM~a*-nuDY7xL_|%l-gxJ`$M1f#1THh9WFQ6V!m**4}UGN8JbUWYyV0++F zpaC$O*Mot^;I=>`fn1QShZ}teb#RC9E|3fy;`wSIRWuFQ2AG(Tk{XthBAAG7q?X)J z=XxSOBt9WJTp*C8gr-Huh6@J58!e>w0yYO40b2l@@%ncia5;qMxS;E9;F(I+2Se9rmONuq%bxk08DZ3UGP)l~AV__pZ zB|0)XJ|aOtjwB`|#J)p1>5qWaHTOI4`BKBpW$|MK$N2O=I&ykktUNY@97ux^xNPWGp3+IS&S1w;Vkh-ROTuAb08AfevT6{9>2vSpG zqeFoPSO+MS=euzgwd&6C!%T>3*FnvCa7@RNOC15E+@Z-KVKR)CE4<#OCs$1zkkTd4 zsol{j=8>@pp@Q~ST=_cYzN)s(da&k5jp@ZXzZ*#1{tC5Gqr#(;*vanbS7i6JQ|{@|3_fJaLQ&clvC33Po&F8w_?)qN10MR4jCA17`q z_Xko7l&h$XGgm$jNcnj?J+l8JLrMdO`f!a`8v4NGsA&%>>c^>x(eb0BF}xC^BiP}- z7&^Jh_5m7yvp?s4LVOqvLc!SNkVMRD!QV%X4s%~kJLo)elhexZySZ}CMx>a_(o>T| zE`w7eV^T1SqGM$h;FLc|>CNa!b9AL3B04_Y983BzH!j_WF_dUCiRwBwHwSXz^F|2@ z1LMK@`v6FzIS)t{lYm&WvYn8R21+SYuHIFye6%VbnTNf&buc_3l@ZcTGvFa(?ikPDEk%2L8YVnf1ZVX@I^(MWFuojU)!4;S62kQDQX*pSFK z;E1X``w?!)z2tQE$t#FW2xkXbcu1-Yi$%(IKd#^pAdPpdgR!GwSZOdcEh0jej732v zlS!?53L5!y6+T5C%I_G!IXnZX1s)d?866gql4=x4dS^lQ8r)DqE)Z*Ob`+2XbxR=i zdsh^s38lgqHqw^Afq8l^3?0gu)kdlgc%$Lm6EzvA4SomtC@vLz{@`FvKLDiiw<$J2 zuMwQPb9pR>P7lytBe`0Qfz%BCfhqs*Juy-6|Mr22Ex>>0iP;Jc{QZHs0WG8mB_zt& z5rpM!Y)Y&^Ffx*B=|>=q7;hlO-3dq|>pWH-EjezE;f6*`EXEfl)J1R|_;-VUH#8p= zQ|K3>GU~D#AUS9NB;7BL(+e=XD194{CXf}XMpziIH4t+miuRqs7{+8@78@I#m>M1S6Pzmk45WyJ zCMU>~L*l~)%57=~7^F~o!4S3J6)Gab4br#*A+i*6npLk;x%4vVRKa!Rqx^%%a4pP2 zWT?esk(aJeSy+^14}k#L*s0+G)JJ;F*g%1x6KK$?nMfRrvvPEJV1>RX<}*;&e`drjo*D>p()=lPYU zHT4s_b}645zaBu$1&OxY17^q_$z%=%Kw3W*11Tnbkxm#1_o$PU`DRVwsvQrc&Kb>f zghoJ@CGyjX5E6sl!j+aB&30;HT6{)WLbz;h9$QYHorN2ULp+dz?2UrdtIzqkt(nfn zq4f+dpBj)V`~+PKD4ofrn*eFzY4fPY;}_&7{SV$w(^*_Y<^eV6<=_r(sNy_eBVcM& zaza{UlptX?*YbDy+2c_dk-l{?S59LIRt9Q;$s#TxG9fXX`l_mcE6@%)8J-4C7(oLt zBrM8{&%X%8P7axXQ{>(+<5rFpaDa{6a>Son=C*<}G=s0O zH#qf~8IW3Nz^89s&N(_82B?BRkiQ8q9b6k2!lO5zZUt-&eI6X33U|R_vbPuIC=zWK zBmU&EIx>;8Y+wB_ugHBtvi!oLZ8-j?-@fsfEX&)2!J9B!^Z4 zX)LAj`DH+AP!N#1))7ec^pN9*3Px<;628MzD)@vC*-C0zyZQTDjdJ^(5rW48P8n`)ynb)+V zZq`mN0DnN%MLy-C%I+NH%Gb+;wA65%7zpBI31i{BK(BC|CkRp?4ju(tnS#qLzT!@@}9e#o{@M11!_!f{_ zGW#Ugf{Q?UKv-6AEt$pVD+MQmb$tFqKt1qGK7BdR0K5;-5Eure5!fEs4j6J8@u!wL zp60d%$jtW5w=-N{-UHH{I|-yYw;k9DSO6qL6M@>m7(U&PPahL4i-ReFavu|oWjh5t zBs7}N0tCu)Lumt8qdqz8#TmZzY=Ges!)-U1dz#kjy$&DLyQx+?k#4Q#sb+fhD(xe!+PiJ9vQ_=Kd(mOJ*ZqXM`$jm75beMGM*Z$(`%lF^L?3P+ zPQSPBlt*2fsY-OS0pi+pzCN*yIKl3C^`J%j=CxR^o-)n#$IdnFGnwTrPWo8n?rR@p zbWeL$uAt}r)|JOs?>u^C*R|QMulG0nkyhQ*LEk#p>q`7217H8N{F(R9?H;_cvV<8u zFk!)N_f_iBqhD$?Wut%eJX5fH({<}fnFdFhQO$Ljyyo4U^g6cQk~P}uZ9|7?vU#ee zP31w34bCXu&e0LveV-dT+WKpLtMnEXV{JDdS$Subh{rs}Fs=l6wK0%8x7ZlndW-+~CqCmEcn&Ej>2PeVuSuI2m8N}NZZ@@lWqtF2`>IE? zqK5AtpY>e-q{exsF0H!f`4?kb4t(q{nb-c|;znajDR8ZUJr}iI$hyzDo%}KhoxMfLo>&g3>gf`-4l?~0x z+`Hv`9RD)%OrF*YBSu}%lqqU)Xh%0a1C?gIW;MAz`Ei>U*`X!wZfYAQFry3%pUJagdyoGz`^AF|((KpY>?6;I zXHJc2U(xI53X5==)l}7arZ;VG-t6~Y`2O*M)I--APs!bIf2F3zh$RM3y7=qs+%G!# zca*0^!)nZhzAYQWdA_iXnD3=`_@ zyHk?u>NrC@&F$X##RZkElj8O?o^f(Q<7;Q`d<)z({Jml9;LE;YO|EucGNMFn$=uV% zxldNsIS#7W6QwA4xy61(i(NxZ+v?TH=Wcjf(ZSPj`BBGP%>_o!C(k%=enI1^kMjd} zH%@XK@qSEgU-`04Z%gv#6sSM8UfE<}Xao`!w)Wnl)A6a{ojHq)E$oiSo=x}lcRgy~$}8C`z_`VM!e!Sk zyi9b;+;~WC+ob4y`qaPz!;g&+AGw)^f z__@2Sm+JJrO;wgY7}-NFdxY0L-{*r&onM7Ndj2_Yyoa7k&X{M{Mfy#g7F4$x?;Cg{ zw>(&<)!BDh=X<>rdYx+NR#`N)>2=isx8G);^6AvEW>LRm-)$UEB~1^opExtP)V;D% zgy%Nr;E14IgIptCoLKDe)yJ~7y=Z(<_mIy`H-DRHY5uLK>Pr`ojJr>|M7O$H!!X$f z=j95I_no5)bzhfj*?t=7G4E8^DYZQfBnfptq;CSFN}V=D+&MCB`}PezVm0=qZE-zc zSz_K`P1AEn-VX^lJxKO)f~&IJ;XbdbPdwaHpyc0tN9b1FnSN*S+vXdO+$xat-r^Db z)YCb&Hen?t# zN;W7wcdP7NCmZk5RWW1IGJXtet6UM@XBImh93+}zw^ndr>r$kphNT*$& zVMaM-yKKg4m22m%|0A#@I5bnAmM*{{nRHuRUV(N_T)N#`rvI>*aUMj8u z>&KdQ?kE-7GZ|)%!l{f+Cr8l+F*B}{gX$yfhkG;joovM}*a!AS*0xN}cx!bKih&j* z>?jrA03&Bq$)-?;sp;$}ifGLA?&2U?*O;lba1cMi9s+G74V(*MKmD&W{`Lfyt+y%3D7FsxkW$zTY z3t{iR;*Lulf}La}tL?{~jJ-32J0saUa*VsXi#x7_9X7$7witJu2On`4KqW-(t(aPC z2k~s|=Ao{HDH8YrIMjwG};gWK?~$YT#VHv!?2&EUi_=N z3@Qzqzp9U+($M;=YTo*H7LuU;GjA!>f7<=|TMK=$1OBIl8Bjg`;ls7x=GDih92?xf z`5OoIpB74?{>C$?fAgV>SObS>{r+Kg2GqZ0MR+ek{byzRcnkPvWiqI4|FF9S>Obv% zf%;Foeet^U&%86B{=;AKIjB8Q8wQS$mZEEQXVbwL_d2s5c@qye+n(Lr?< z#9r)lK1z3Na=D3AwHC~YbWv|ZrWW&7VaW70cTlaytB)O>NVVTW3oEhiquS)S|LAn>v<#jc2cu#QSx>%$(Sn4976I#Nh5&=9q7tb%73|Nn zO?(=93OQebQG3{RTime|*H$j9$FdC{{ z1&6`<{Hj0&V}x93z&Ll8itWI-3d-H3!d%A2#ZlbWoYUY!q*N4S&g>oFAUb8vjB|BR z)wIB1rGScqa7PVnz|eRq0P~@ci=X3;>z?vW*6LlAj?mIJ1&qeDkRA%+Yhbj{sZfMO zjk__mt`1^bJi#dTBBnf1DxAP%40IIUU`hu%iuLh$AiEg)E>dw27@1;&hNpIho1^$V zG%IM0+0N008M4HM)g8A~8J^+Yq+&VGROx9Z+`*K(JBo}fnOb)TaU@0uRfDj0l&Y=( z>&awU*oyDsjsm5|hRqDNsCrd)h>QWFGHPspZUv)WLeDR=RtKRWfu zcd0OjvGH;g?q@Ra{e~&UcL%1%%TYWA1t_xY(?NKQ$?$d*H^xK2ku6ycJ8ocnA0n(X zz^Hm|;2i^_>d^oUyavcdX=29NNh-Dn(RA=0Fo!+zAy>9qfpbB;XbukuAgwYsqvsW}jH%+@1EUd% z`34^yZMmL8Z-6C%ku`1tYy|TGWBm|+`)KoMl*7kWs=hry`!Mr=Q8 z!(ECRdfiVd&H$svDqfoUB%z?>rwH1G(J2n^MD|T^KE^y@& ztkprtHo6}Z^ehss*c_3s9Iu?>c-e(ha84Gs)}106kvb$#s#J(*imdWfNQ#dt$zd< zIm*^AT)>o$a1@_~M(sl2um)SNAY}UG_f%0+@<34zgP}(G-hlf9fbi*Mx>*7HRd(NU7d!gusKr$ zEqRbYfB~P~k{vob!0_M(tySThGpMYjW(-pLLUi$Jbze{n-x0u_UR>uE`YAp8vQ z@2*Osp(5K~8z8v~m0e%zH%qKFRJlgb_9y}jgMd~HjH;brzal7lIh5I(=pZ&2#=Spd zZ-9^|gHcCwJC*HVR52cpUQ&_qaHcliK|F0Z*SGBQE4~CqA;WToB||&-S5G5=sbDk; z1WY+zxG(W(Yy`!+Be)TZMP#s45&*{bJKBO-vy$y1(V`K|Uc5b?MIyR_dzOh^M%H_> ziF3iQD)MQUz^KRA=Zi#b)SvHT7^M9Bm;+SSVN~^}IuR;OF^KillTfkZ{7r2Z@;eKD zQ2&{C3Dkeueehcg9YcR-As#9X5I9!Pv%UYwyOj}Zg#GzC_Gi<4p#Iaq5~#m%4C+4{ z(JB0Q4aTpD{4MWWxmW|@Un?WOWWTG+6)HPA{?f2{Q2*+|)}^e0zgnd?>yDTyf6Yp2 zJk)-Fjn{0kIz%kW{4i-6`D^1b+gcrj#tg>4DNAU+u&cAU4oNgB*$shM8pTz|Z4cyN zF6>#(1Kg2w-0Ex*t$ec3b74FfMVZ@)oCjl@fkj9v){Eg5EUa!rq^cfZ*uCP(l8rm6 z6;B)!sdz6KHIjQ0h(>e69d%(oZ;cfQ@Q|aL!J5Y5sOA@o1?&5ZZ2|i;=hwe5=Xh=% z=d4TwqeyY=44C6DW|&alN(>mSh*;#Dq^iro`Z9GHb}G2RiBXQ7dLlPbSxc&8!Greo@-MSwXg+aq2F#&wA>N-BCghS@vOL992HGmY_wC!PFr$)$|~<03=OF9IWX zo3pPx-@xcu%)JMjkHaY%%P8j(Fk3b!+0-4+c?MtbLOBl1nz8q?6>rBKjdyNe^%aZ) z1x9|r-R@vG9YouL!Kg0wm{mL*jCulopsuT6G_tr^qmjkUD2yYdx$%s9MCXA~OS#AH zAuuY()&3dGp0kPCuS`(ROWF%HmviyLo-9!+_65TR2j$8~NX4tbu)5L!MSZH-TsiIp zBNWVzOT&Bc?QCZ6bO*6X4p)GE4HZuVBbT@>)oCyXFgyZHq>@GxxwT0ROWv=Yh-3uR zKFISIRlFQ3brjc_8(odq3<}Ao6GEGTQwETm9fW(#eUpbu(@ckRD%s+?cvQ_;+Q88 zbOlpK6q|5wt^LF*5PLFZ-E76zamO`Ch?St(G_C?}Lo))5dYWC|RX2m-d~1-c_#^IU zso}<-^K|9Np)JvFus`RB#SCth<#MKg(d6gYZZKz#VV~}TR%gF z8&&M+Pz?aHX3D&5RcGT4KlH%ja2 z%#C%{NCBNG8(}LRjyoUL1$r}j1m?vu=XuuZ3zZ8hExjpVeYm6qYYhjVZ(i zO0#@1_e>>~K20%bP3VUM;xncVceb?(2+m?GY{lI+aiQm`&jjO! z1qJ);FGkK76#p6wKGy0WBRFgD`ZyRDeahHwGdH2RNcezRASc!cXKakYxYp8Y@)gVy zX?O;wN+q4QaP3n?ui@y7U(4d4(h7%FWs_7=1V-^`fIM_;gURD6R)@$fU$ieb+R6<_ zZtWe#Gdv72gqQLR`>6HS>L9f6aBHsawqNnV$~F?L51U4TSOG>Aa4mWS#*GddE3LQx znlTvteqc`QgR2;KG`=`DK7jQDJCtzIM-O2fj{>8paEBHv!FsZcV)O=#?HP7+X0u1>DvgLV zFe=CGm`cFNNBj@~X`jF-wx}USD(+hP%VYGyBrqINENFe;N}BH z7TUW4j7AoYibq1#?19jA4xX5Gw; zN(HAxj)$3X%N@jH4|BD$j~vmq!%Qv2hDSInm>00(0!F^TCdNW0m^B!}ho3L)1akmG zY;ach{s_~1rGv=yC^K%QgE;Ic*TXP9SgLv&tREYyX2&=S_-Vr&Yju#mFn|qzkTre; z)O(eK_!umuq3k1XN6Ki|?X>RT!C*0f$Hj2%O=BetQ!RkP#tg1R^>DM&qE(v!-*t_Gn^JdRr zTb|>$S&VRywF<}}Hp4dDd9rsuaK~l!Jg>~^Jjhxdq&w>~rjw)s%p6QzfM1qY{!C2u zGgNLDYkEOB0cbsn2D3qK?oe~(pUhn%4ma6y7zqO6R`}3WAE`Xn6E+`)IkW1ZrY6+|H`$9&#FQP>W={TDhYA~-85+A{HLgFKNULPqE;rKMhCjlSImk3W6ZxPFJ zOZW79=z@tvaQ**?Z2fsJ_jkG0v`u`re`u`6Na-RKH9HU++Y{9prupSDT z!mZkJN?ysUgcO%G_>jI9AG!!h$A(g%hR;@f=&FyDe;Yo8+wq}`P=wD;BDj8oTwo}H z3hts@xauPr!0v!`U@wqH?*V-1`W=d)V;N^8a_4n(Dgf1hb}}W z$|vHn0TEn;RJSUS#+wG8F5%PbBQ>TuuLHSw))SEYmkhr+m1%)5YOF4hDs00Ws*j|% z<#j@;&=^R=p(BuzOz4Y6OD>@kFVshpI^&D53!hHdfT=sC&-!i2tM!qpw}MV_umh62 z&OplAhra#;BsX38bV9lv$a6xvb;B3g_Yevg%Uzmsy7%Jq5YnwT&k3o?gLzI!`TclK zNa+DUk^=EX=|hR|NVmiAML3+tU?RAHt=WVTyigw-F!m?7r7Db1{m+m>6@z?KeJr2< zcSuQbe0qJP5l#O>5827$(}6ue|04W@Q~R^|0)#>)f46=#2@`phkZyB%oW$#dMV(n-_Whzd&vt|1S)%4*gHcQv9`Igm3iqLIdC#2guJSQZB_j!B-q@>66^*3w^{t-IO@EVFg75vU;B&6Fq zUZ;K`PXAIBp$JHQ-x!Gh1QPn#^h zft0>@qyi2+C&UU?$xY8bV3g1gUlQOTB7Z}YeDOui^9O1GLxH4+NzI=OQFG7W4dfNYyOi^DX1^5t6=~ z#}z!T1U7=c2}oBxH2NjbxY~*gl(U_Fo=^B4Qo##+Iw9R&!WX-(0a7~s@djQ06{K>v zke|YFU&I_fqa~+t_mnsBUqOmZHS&|G*FbXN4PTy+Zr|~|K2pB-eELToKZ~%3Fhqhj ztkRfi2c(SpJQ`4KxCqJ7jzD_jp|zIku;kPKJ4i)(;fv6w0k`(yhO4+YkXq)zb4MPX zfOHXB0ONra#1uX~l}{(62Bh)n<9NM3Qu=saA1~)KW&^2(Q-G9VDv$K9=ixuW44%&< zf{TzGodYCC=JV+bcvSGX5J-_)#Ph{Kx(KO0`3gQ^B`*{K$zU-M{|UD8xC2Oz?c{k0 zCE_Ba+r9WAhYs+(97q)(;q?(6-p0!ZawvK;;YhEI6U<0l@!0%_+=R;eNpS?7^%8{&%$sqy-M z1<8(>&risXKMmfHgvTa)20|*(jOX={q!xU7Df?qo z8uA&8cr@m*J&>;YNYk(@uh&Pi)15EZgHNxI2&%w}*MV}@a~obDq+5G@ks(JOok_%1 zA1S>ruM?7C7oOKgYViPGuaA`9O@*KTZhVH{AywqTrxQ{^Z=U~WNcjfy`3T7oU%tFQ z&jaLqh5%j&syZ<;ojEDwqPKQ9m9? z9%u3y3CWQOyiShOl0AjzQ+dn-(nUz+r}3PSDxL+Ta`QQq3zqN+EBJ&WAQfB>r0sVJ zkgobj4j$z7{|$=i9rh#)QOhcURPiaEpW*QwkRo#tsJ#Bq4kWw5;(zNzk|{i>{qI>M z+Yb}@-uO?Do8I*^P)|lS%)e)m|DHu+82oz{$(~8l)AHZ5 zNCW{F4lCIeBAd1jxCs9}i{xVQKR6QP9@zhbf^3cdn{u?w|9cks?^z@bBs!a<`TXx$ zBt=6YptDJe$G>Nhf`88<|2>No{CgJp?^)!(XOaJ&Mbeq%|J_+6esl4^eipf7(fLb4 zr-sAZ7##TNILrU-panM$oNE2zndY*6_M=VmPMv?SH1=k*?MH4#MA<}7IFi+_n?=&L z2_?1b{W5h2cQ(i@-s90>e-u-EQAgf(YMp2Q;~!k^414Axx_znNxo*B2d{)fZ_}Msl zO;4}I`GqUDPFYxbZ2Z^-v+eKo-aO?+$mbxP)D~S14e&krEg{8#-gpE*n*IRN$*%HJ zS?TRGOnrFHj45r#jGWqO`8T@`HPJPDybBMRO9JXP^zlef6%0%G{7(1F#;f~|44!?@ zb5_q)OJ`5(KPhbftFQp?_IH@zOFHs^gLm!HJJi~?v>&%2{pF>1&m^;>atwz}Xl63A zTSD`+ic8b`ep#_RZqU&6-d}aZ?e<4G4Rjr;h@0Hq?fT6Ym3j5Oqn~OkFO@C}@^58o z#(ivbAuFk+>CV1a*A_jq?2~b1smQ#hAXde^eDT4j&A0E=Wf~iXXD-_Oy6)uUK?lF= zDi9b)b}ndBbhafUxvb++;u$(H<+b#Fr!xoIsb*&31yqkB1kIeA&aWLS%$T`wFMv_MEn%$gy9+xohG2WZCf{=xnlKfx z5_LzyNS?ZTnkj<~RQVy;-$w6Ux6P3|_1ZkF9CSBi%A~%Lv#v_d#|th>*X|uJoM))3 z-DKyE!zQhn%!#f8v|OL|D<0~qF`|z~*H$gn1o+>V>3C*@9?=EAFEbYViC1qjWVQhZx5n$H7-U72WncVo)&-JQ{`4iNTWlJITGRN%WO zWB4LKh<{EI-@TX%_?9weF9U?uOb)(nm@0htX1c!$5ZW@+@NLK3$G1IW^BN(3jLvxN zE_7s`gB3lIFse`7h0cug8-)0&L>_c$*4lzUti1E$3Y$h*Gu!X>JZtl!x5u=e;~MRq zrx(z{&Z1`MZP)H!Ca27md^YM`_{w#FiA7EJmE7(FRwwVAR?Rk1pq`Cp^~FbdsYquB zJQy5s^Wd7#y-t~RDtNvnu%?>tIG(Fwo(tOsg3TwANejxeF zw2H3I;^R}cT3+q5#4gft{H7I7XFq>xzgol9aOo7M*?IGWHa_Y5<&4__#fW#!cAn@S ze)X)~&>M|sOFKQ^IB3DbZ@1_d4e_746D+#?vI>W*JSOjIwl^vH{W$5*Q3HeTjajv7 zmzqII@oCS5ZQAmNUB@mep4;lp_M}aLC+>NCUcdT*<_7~|=1Ae3wO78K{+{sc18>*> zMhDb4oL=cRKUm94D`Me7qX6Tqe9MX|AMXuMYdhV2Dw^KAiSW?!JL5Z#2%4xfHR@FL z%q~lRJhmRWxFp2bz)#C+>iEiHvWxb!KXi7jZ@BZhBa`%o4-s32oRyxOf5hGNam)MX z+v^k^DzxxBz^p8LsH=8SC&jbFv29j6^4|Xl{}3}ebkn(gV#{{7E3Vjw%6NyH!REmF zb}yeuG+s5!>%{r2=XP878&p~nbEOrycxWtdrXMtl{OcU2Z6Ug_fN2tnb`CUElRDikrV* z*Sr1k$Ft@oyn9>SMrgBOz0II;OS`GKCYwz06HIJ5L>^uiw0f$!N_F#=JA3FR%-H6y z``&M!T4wv1TsYZQd(}5=-|pqQ)p8#TQOltn{c_z*&X{!z8Y%hEtozJ$Blo54ynJW+ z$KJtLYPQIp?XOrd`cv2df64cBwV}s`dunMgW*c)@yXvf6@4rm3Rlyd!O(xB`bWAeW zXYu(W$DMByvM%qvA>JUVHmmav3Rv1vEAfKl>&x#R`*m9S1)7#lJZ_{J*rtEO)HZAT zy<9n+HLSuK_Nnji=LNN!bdHyG_nf|acZ=<ko#w}tUa)LgMoE7!w=_68Z>>- zD|cJt28AatY1pb9bLcSOXPWh%3#*PlX&i8%F&_!C>-!hGS)x^9)* zOqh}K4pUy3ti8Ohq0H~VE&mRoZ9JD>*C~$dI;XmI(aFSM?@1@I`T4Md3N4}D`}7uzRXctRGIU!yAoNvF7v5nq98lkIf!;&&b(*_Jz6$cx+V9xG$D_@Q z;j=$g&RMnonS}Xidn~+s-Nmqm=@qfNAGoZ0-`!DeP_DUV+AWvcg-7zgcd5HZztAhB zUjYE(W0DkXO{M{h{1^3R>J%HK9@{nXHF364iQRGeD4^0>+pmGZQo?Hgp?8ou`G zE;~gZzrf8?=S;b3eSY4+jUykN_}sHa(ofcIHv7v3LBFihZ?QDJFSMR}E%oZ~E`K=B z)0Ff~Xf?}xb)o&1sGlPB)*Zvdx`ycMYX-vqEfcZLj133mZgVFpMF#n`b;*o6A<6v%a zPos7lG+$(OwQ`m{Q*VCsn102Jm>yc|)(13g6|FXJ!~ETQ+stri8}aG!iY5<>epqP_ zu?d~oiqy~mw!o#J9BooY^Y#-<|x?TC4GUq-LS>>9AAEL^du20yO*}QnS zykV~^(_6-@8*i*TJKrO4q?@8>cHn*kYek0!!bFcwYgW_*7|OLZteS~u*%vQ8IxY3; ziEV)>b|R-wot_n(nas3LTy|H;v>OGV()UH|q&9p9&Q#Vh?ZT7B3R?=wyF z@(@4McHg88R9`3hHgS4U?091B-IJS)Qk~QNZ|=FIBGrGq!1l=M(DjzxsucOELLG&t zDk@s6C{RU3t_@L99SMbsfej&iC84+>gk_2v5{lFy1gk+zh_8vY?f198jN#%<5T5vb8Ioy>rhyO#+$B$1Y4zgL)QR7X6LzH!9q>ol&SqnJ*A$0t&;(S5W1IS8w=N{%V%%W^Pe*E()w!6L&GG|jdIJQKinDYey7ka z_3*3?m%g2PbtAu5@#?4d_S$Qyu0_f^2~U$h1R|Mg0|n9yq{GX_&9cR>RjE`D|H90EgSD@&^k!N z>%44Xb-xj&OFy;LQ+d;SLrGZ|VUe0shvLlO^5&@nGZxz&QjeUVQPf&z$vKZs-CnwO z$W?jL_LeEr?O3S(>2^={&iBd<_!_1i+S+=l>B#8nrQP@S8|Y?RvZkldK&Uv=3o$WK z=t>cSs9qE#DFib`1qnJ*2y5&hbXL5xgK(0BA@&#nbdaFDRDxD@nP@TRgQY=_>shx( zU$xIL6y%u3eN&&a^G5RIpykB_&zo#9c(*W27WH^WMe9vd*QfM$oE~kHv&pcfUAJ@5 zEfsOr7;3v5V7F_1yAKU)cbqM4KW4+A%fkvc4K4no-=2>rHuvu9T^+EzVOphLQT6>u z1GmBNd@ZhRG-`4G$Qs5i^oO&}o>;Y??~}HRPAS^j2zBH^7sC#fw_m8UWaA%o6B2Ie zo&QoZu>a@?~#Jjp7X4Q+%>Td@U8zdLzDhy%FD6y&+gBY-}M| z+d`OZ4MD1KrXF}kf;kB`iVM~d7TQ6uvVmZ$m`g=m?IGloV6UjMf$)_Cdl>a}tndDe z=kw1UI;hZ}yLRh^XO&+sMNPUoaM3K0_tNlXCLa=f&&KM=E>`;M{@k>rCVhxS*`yzL zO`m0~Di3aVpz5+sTTu%|p#xUH+cRe5O^}TLYRGKgyD)uGQK#wFm9NxXGS&^r-tK(S zUwvoZ@ljhQP3Bnl?`YUw%K-dF8zSUQ+=Z%Lo65S@#Mz34ccT$!~zYkfHwrkw}=zX~1 zsZok~dg&&?W;X*e19LjW+dR0cf6eyx@QzwvHU`+PH~Nu8e~E|x*b}w^^~18GOXGg_ zmpj+a?ReW%O)YxV*3fvb%4ym+cZe7C>$Q0A*k&PSBj2q&D%W)D{3Ey4Oz*p?^MQ7G z^SkJH_j4FHwC8^M6Hy^ff3h(qDKc;T$7@%7?}Wv9Y<~5AUGjaq=Fc0d8Q)Zqtv%Z_-C;Cqmpfq_sQj~C z*6^SKr?V@y9kLzc-~D0rN!ViRii?f2#2+zA^wue?;I9A1&L)I5DxYmFw8+k8~t7qfHIth~D$ zgNA&t)@UV9xTU#sQD61cfv4lOtxt&Fbopc6&Cv@A)`YimTaZ0zXZHJ!UwOj@u;^aj zu+g!O&i58xSR;KiDYNRktHbLpb6zeE%+|*zUqgu)`z6MXY zddlL!r&qUYTjY?NT|MD&%0p< z5BBmoq`LhkYjZLR0gwp=aA1udL$rGghviH*IY2zHMW*ADn#Bsz=>zKfODZPbPjC)vRiK-1!1g zmfd8(kqrt>mpzmfd5_h5@`bgV%^eBVw|i1w5N63ll{Jt*eYNPJW{;PT6XH&<-`_L! z(J5iw5O2RGPrK#~f7+yP#ogOu#%y;xf3Lk)R;Ie7*;DsPdSUW*6ZlAQM?&=tZ@6`6 zY~sMFyV}NgIJ~*Zx;3HKeO=5-zqS075uaJ)F*#SPGp8_1e*DG5Gh@BCd^m7TetB-G ze^j}->tr=IJI#J8xu3yskr-UxaFW62+-}n!?VCUHS?oEh^Jj-_AHB|Yika7&!F5w7 zPCdNRdVO`l)7^)5yXrNLFj|rRGVX2H2fk15tSd?I)Gc{-l72IgrZYv@x1L?O%Ia&c z8jg7Ky9_*v2;w6 ztqnA0z8V;}X-(8;kD={%sM1dc@gIBW=1=oad8xd+oBZjK;qf2s}aw zJJPF7;G#_1x4)Z}}r$_S7w~zTrJ>EXtd=n-snE@|=A`o?85<_R6=_ zn9^{?LYr2TLa!dpYq2a=9yY_Z+$C~i#r%Xd@9o=P>o93gsDYO0myVwn7RSIAUIJCv zztstb#oz}(sXI^dPVU1n(w3y*P_S9`?eY*>LuU(*4GRgm_{niD?m+D&<#mG1A zF38-b7A~6Nc=N%=TN*wseNM#nit3@Ck}+XyWTVM_Jl^CF+0^0a8O;#89#e0#hH*#; z99G|OR!-zO{7(=Ek4?Rv=iNTXSHH+=ec&Mb%^V_kXWux?T zVxreQ8Zu-;|7l}q$VWw$y?(xuJAdR(-RK`EP+ls{xBaY;X3h!M51Uz6l5xSo`~K!u z6Jw-@Zcm!y+WKY5p$T^$-I{oEzx0X!sE$hwY>oWSu6;4jTfAEG-OjmxMgLo@!`!J` zaDBTGr3ciqF5KH?J=>z&QU9b)B?+07AMKggO!u6a!r$n0iAfc7>3ns3GAx3Bdy)q$t)8gplV0LE;7>O%db< z!FmvcG7`or#O@HDkr3w&AwyA0!otB2+Im13uZZ@5;OYzE3<+5ZT~7#KNyzeqAXik7 zP~-=}+zUdEBEt(p;1CG6Nyt^0c|%b5hmh|LVX~r%ggyRvn{x2M+tgIWG#?020m$*n z2RWuGYz9Hl34~BM2*M1-a}rLH;5isVzM^0-g!CW?btKGI4D^LyIut^&FNC>@8WOIP z5bOtmQLOiakT(p1WC(=$il8A7tcOD=BSE1M`$Kp}LYzN@0!1ka3xgrF4S=v%5gh=* zbp(VnBor!i10j4RAuABVGDQUmMI#}Y2SHe&$OwWEI10jT5>_e9hC)yefsj8G!Wu;t z342Je9|mEaV%jhWQK1lCk+5E2GaQ0W7=*&%5H>2FlW>v*&tM3}ih^JW>ERISNZ6tn zI0AyH3_|e;2-_4jBwQyUcqD`!iuEHQA2IkH@!?o)67z(YdC)A}(s@)GcZek7l`SDH(ck%sJOY?J$=s2f`B- z=8@RxS514}d#RepGiPh-ijJQ}u`>po@VL6|uvO#CdqX{b9J0&(IP19K(7Q|aZyYei zYKZ>6bx#ci13Krl&dTql#;M83PRgZ2>TS# zp%7f7A)FzhOraYF;VTJQVGs@~Do7}bfnXjEp4k2(fgxe$>QJBdfsK-Lcmq9qD zs3Ktx3HA{XPAH~DK!}Qi@QQ>Ag-s*`op=a^kq}NPo|AAAg2##(R)gHVzU=n8F*rRz zd^JOL|4-YqhN9zUr_OmDTU&N!UZ`MLZFRhrh3D~!H@bTnzFk~mwWqexmX1wB&#zqL z)}mej&L|3^U^YDg_0&b7o^y(U(GW}%ArwbLxS*&Z;W`PyF%T{(*2h4|OM)O74dIF+ zXfy=tWC&#>TvLc+Av_}?E*3(SqLhS%DG=JmLAa@ij)UO(|JZvIf2yMIkK4VGp-d?e zGDQQKiDW7yO^7mQmXfig$(4u-Npu>d3=KkaDKaEYO0x!yXi}0&^Wga`?)`RuzvuV; z3!dk^dbjpnd+)W^9?stToO_+K1d1k7JYnP`p!iOT)e%rUW3G_mz)~p2M?%rcERTf3 zJq`-vC@9*QTci+K21N!bUNR4(pg2tmi)f6!*ZgD8*)we7@A}M1dxbhK#0twBML2}* zRzI>XdG4v)zgzDob;uc4OO%u=eYD$o^NidzaT|{FE0^QDqtAKUp7ynWZeGmz#N!fP zwrTl}_qqytrJF9j8g=Jaz^cVB@;l0l?jM-5B=?nX(6B4@Q}k=^D?G3?YnvH-s>;=2 z>%(@{?<=j3<)6EHF<6x}blUe^+)*nZsa zfy6JNLDkABP1i3cYZ0pBxEvOUhlao7J?p zzI0&stFrp+AlIF`dZD9-tn`_o(vp!?yyZ#VRksNhy=O0;Pad6F%-KqXIQKFB&kN0|#FBS9QoLT3mF^6wlpZk4=e3`(7q1LV4`sar! z^qN1Y$oU>F_2Bw@p`P!|;uUbOXqWm;W=oOocderB(Z!lxB{4oR1*H|*wswytMNJC* zXOEgO)ad2nQKhdWYHii$)oJ7!1UJ}q&9D~=+wA*#ktm;gANbw75me*V{OhUIP#1@q z2_HNbh1qQF`2EdvkGX2;(XxcGwiE02o(@SVk(W*k8ZlJ8PNaSCSlNrRMy>^_Tdo-f z9$g;AD6NEhx{*U>D6PD`N_Kkr-OPu!rZb=9-WAweI4@*M+P!um&V+B}3Y%82T9n=y zEdQ|Wg5=f?ty|(kD_kmOEtT7Re^<9ApL-wq-5Yga;f&59hl;H4-ZRV9em~l|bfCtV zCC4>YcWz8GNw?iycw%0Hz%X0+k5PdWKgsI8y?x+j{fDG_2ammxUre3zRW+CQewVWDHo4YpL zWwL{&O??-UIj71rKIPddne($PR-0_!8QSc%Sz7Ro|A&WhE-~MCb%`-8Y5q%IcAfjU zSk32zaR0Zu{j0_oe)lTM6p~ulzTJk$p59!uYFa?i``hmXSD%(z`peeh*wsH*KfH-; zc~h;gZ7&pxY@z1t6YA2Ho)8$FzFSF}LxNjSE`SymL4G9hWp=?*{n* z`zBK#)3YvDQiaF*JFLF)fb(l-$Hcwy{>&3{uW`~8R{Dm&S&{WOrnV2+R(zzxuR!>X z_B+9D9pCbg_vCtY{iklhPgR6bC$%P_ibV(idNnUs+Kw4>$3Uz>ej1wrb^4Dtw~UMcJa?%(Jq@! zk!6{`9nY)h#vQ+Wj7d*m$#X`pT4NV=T%v2J&Jw*#quvyKE?RPJ#X^T+gN{|M?lf9- zlyisuep_KiXXZD7ggK9ze!9H8b>QjEwQVAEB>(zt*QVQUyvF;*@7?Vou3uC()dU)< z4EK;Nn(Y%(8Fyf%$E}{IB#W!hk4$D77m zIZrPLsfLN|_**dN#PAx^^$yaX!>;ImJk|dUxc;H{hu^)Gt<_RBp)BBUw?df=2orR4Y7~>f&=q=s^@JQuwcP+ z<~_Mr;G@3H%XN!RlFyHED~3f1_FkP95O-Wh*ZuN=v?(#8DmInL)Lt&j?de>9Wpv%R zjjBInN2&S7j#Bsd{8w~WW5*J{*6ZbW&t%U4;R}J2T&_Lz8FqH*$n%4>&)d%xDpX3^ zR2aX~@u_T?uI0G+QGxRQjk|^SNR`Q{*d7cUGk?(Q$mWvfaP!Rb46`c{?lCd-r+w2C z4Wnil6fa$qyg^pIVu!QEQI+`|drZr~k13xF1h}ag5&Kct#Y%;$b zvRv-gji4v}H~;!uj~=<@-lH5u}q;OvfCu=q|esEgY)t$GGBy-x|MG!H9nHO z>hz=GQ>*pm52QDj4x8Wn;;lqS_w2w?vHBy!I_t-p_}4Kr*TB73uT+n0{_UDIr?ygi zs|4$MjC=A#Y4x%Ze%dp(jSihv!7hukTBYh(pdb4ndxMP4lfiC6AAU+zDt2v;aqQZ< z#*VM`2JpM*u*-9MmZ*!S%lGf$t`$E{gzw7TYtuc|Zf|jBn`oiaUWtpd$6ZgbmRHx2 zkI&UiU3@$*HRx}L+%Cto%*0`@guoA)yE>OLgs2uW;aL_{gl!) zG|*_wJAI+f@7|}}-O3hT;|DI*E#@$KqRw_rH&arBZ;Q-v9j}wu|NOZA))V4)FLR%= zPX4Q+N0+Y%|8BH9ReQwp!BeS>=lSZp*WPG;w^04$2GQU5McU+*k9vLW$@J_|HhGx6 zXw6sqWSK)J+p3%8cQWNkaPO|v2_a|y=|7hod^~Z*id8E=NJ!dgX*mk%n!BVwZjIVA z!QFiQB`L}IhFzAoi_`5boE-%{-FiQM7hCZ@`;+dp{`(aD?g{g|C%5Fbv8ITqL}ye& zXK1@gndeyhq}ESQc8=Gm6gsVSqH08P8|#&JqP#2fXN2*FCl_VJi_}|}*%UW<4>;&B zee&T)jL%xQw@=W!ZAkpz^MUT8vU^1LPtVYQa(srk+31n%AI?Rr8Q0eDn!RL*P(ice zUp-x4*)G=UXLrwSYn=Sb>fWDshDWa@@O7>zzk9E2s zMJ+E5*YDo`b@qtNsQlBT)<2PxpZu^cx7Fof$M&7;jz3+!X8P;WNS%OU#_td#o(%Uk z$sW@#E+}(6cx~6n${>ZgLmyZ-Rf+a|nVpc3IA(xs((eb^oxa6aK9qfUBQB@?MlkQ> z;In5tCC;TCnlCP!^R@qneEqFQPoLu*f@Lzg>SkdBuKtvHu|8yj<@YGLgZ1ySC9X~T zq?UL7!n&|Ahb8X+xWmZ1=4k&A{P|74_}aDI5;21(1Q#xwpAlYt=Q~qP?$v)$A9#1& zgxV@c!?4oc84pb-ytb2G%+HgG!7yfxqJ`LypA7Pq=oRn|=xYB=xF zH8?fxgOf>76JP7`9*RdU3NAaT_v6)qt*IxZqAmxd)-0Bo>l>XuaeHBqYnQ&?k$sas zXI1Z?H9<^1P1JOqvP@?5v6VsH?i~}HSLTJje;48B`4PY8hkNflXXmK=N!{gESaEE$ zweyP6zJ{>_#?i8lz{0Kh-@Gpt3QSeZOx^g!*+xQS zg`ar2Y=D^K;5ph&jW?pdJ%9Bk%5gxww#8O2`Gaq@)Q(%3R~Yr&d$evHpL^2$?wy#K zGvD-ZM%RH=U#ghJkJAm)7tQvIJR8g|s7#OW-Bh}1-s&y6f#2kfirSLyt&5M1`0`UV zy21Ty+IIcVnR%~-&zs#h>W*(G_|)y*TvE{zE4|4DBD*rO*K@(#g7lM!QLLh>y~ zcqtZjtoh{Q>0?ti-u39+mlyx8G@mVh?ODg=u9=T@=PigGBYmg8>si16%x#)__qi3I78kNsxH;4-# z*j(8XH!Jg(S$9TddicJ2)uy61=H8z~9PB3YwH`fik#`85)T$qMbJ151p_?<7go$kG zd6v1?*Kp&)UgOti=GsnKeDLtIoauho9?72*zA$a;)Z4ZRc26tr1dE0)7brCs zFyqtU-tW#S6N<{&vm6I*-ubBEuM%To_ro@n1=8y3$Pf8?}Mk=*e6@iA8`iaU-v zGrM;TY&sUIwCoJHW@oiE3Ad~mH4Yj}3~X+Yg=L*@y&H}RcO zP3qJ|`6)G;5z*t5->rI)^gv5s%&)=)-OGA4W=s*BXL{*!CPX!0L z#Q9#!oxWg{!qhGNm$~x%?mbl({aQYv=w$NO=De_^w0#f72ezk0XC=*^8n3U=b(%A2 zm7}0?d7Atg;S&>ES|pe4w04xSpFLb;#Hd|Uyqhzt_A?pl;a=j5eHuyIPwf4CP4QaW z#2w!xvhHqs^+9RZ&&x}Dgu^A%$K{=(_ zzaOY?IMl4V=w7hP^4WrOisr_C&l)>xT>@jV0q*JNt4%44Qhe@|aUgWlz@+A#4uYIh zg>o%B^(3CIOO1}x*Q5tKXmFghaIPB7xyUK?_O$Y_M_Q?oOH|K2M=HV#;&TG z{?mN+@D$_y6}j)FPppvOGdz;tu*q}5X;X3x&Nz6#zNDr1Gh$2E(|hBeDYP5kb(qrX zJA2y9$fUC#BYQ;md|cP=cV;}J&|0)bUC-QH{N?uXYN4&#e0G)TA&k63aDR1|Zti47 z^;1GyS7e{nv!3~4kfFIzN#XH^+FogPN6(mDm%S<@Mp~6fKR0N|V9k0G*8bS*K=&P^ ztQSRYx7z#v(xAViRQMgvcJ2~+9H75@@YeH%$=jw$4Y5wUuy9UXT%O&%)nS~=2Mw1b z+PL)WPEa3mpeeZAI#i{ge6Dle)%2n}hu2Lta=mbg&trOiD(?^!y*VWP_UvleszYll z?G8U5pE#!}{j-(!n)yQpFA1)29yO@ZIC*Q+<~0$nLdGpNo(U5xeMQP2jqf;nY*9$c zTj?sY%i;d@ni{|1>>i&6bNvw0Sm)K|Au*qp8J(Ot`;MmTUBx(4zhl4OzyDQWZQ3*8 zh0XzkAMO_l8uhM5dXM`yWW$GIwZC6K8Qq!7=de1z-5SfyN3>)GM}PThFj8^q3)^dx z26MiziF|&|cbZeL>$<_Q={d`0yV%bgCcZ6hyUU|h4bx1ESXWCU=Vm8nO<)huY34IL zir;WUe5*wDW1}G_geSOasNWA+=`=_ARb9iGsS*NRbFO@|yC~3==Ft0el3_&A^wtBf zTz^>q(A=O@ry@|`o1^r2UZNJCVGVx6#&vBHwpUO0B>ZYm8lyDYV}HXAIVz_N-?Cqby3t2 z>j9B=+Zwym7kMQ#POV&8IIwj=Y1psQS5uY0Uv|y=QPw8W!TiNy^m$He(!L7bpKiF`eDt=*K?0lJg~+Xa7nRbvVVC?+zuNhx z_cC+#jQ=y}!qRWy`VOf!#vlLYq%h)};9kkq@YAP4RK+c;N2MpEL|&eE>U&Uf&GPLJ zJMUEPav6B3`sZV|=$jo(f#Aki?*|3moWloi_}f^<-#<26Ke>2^1)qCb{O;w67u4on zPCl{hjQ4r*?zHS>rK!rpok}`SwI|zto~btWzCzWXs<)~fZ{Mm~`O`Cc8Z2h*-C6Fj zWx%)CJN3d(-|=r+wD}FcI4DvT=(m4#@rP~q3vNa3+JEkif#2bkXDi!IXxmTAJZaoB z@tS1xFlpJ%pDusyR#}&{HL~HyYR6V%xeK$0u3Ea2&*Aa^V}=q~K0>oL(nMfotDsN$3-S3c511g28CwZ z9JqUX(fsY({_f5#9Q&pudx*Q>*ZrZ(j4EOdyp!12J?!C;BZ~$tZS@l_)j9v(_bWNf zo1ygh4X-k;mCn!lBA5BG)xJCaZ`X%6UN7HS7_d_XG7TfkQ~f5IotbiSoyfj#)jQh` ztKW+s)RHZ|+*5w!&KdT?cJKD&@U?_Kzg>y!#_(oSud!hbdBv08JQKYp=gHCFXv&|t zxp2%Kv1XMcx~~jNzdTF*VY|$7Xp-c@dXdR0w_jT-bQ}_zG~jiTzc`;^1AfD6ZRNJi z^AMIkvZdWGwqvpF>a>Boc7MjOzZ#d3AW7YFb@8?1LQYG8{ zuOq{?IKMZxj<4dsrDw=*ICQ)IhevvIwtRi>KjKQP@d>#z!5?0Rrx%(CO(@m4Yu4-4 za(Z~6>S{^d$(P!sYCk>x=(p}ot;r1`$-SehlS2#C_#8Ikw`;Awck94uC-;uwd|j|j zsJ=eMc!c7EE5l$L^(2(HI@+#Qd5|W#yYPr`lFX-3 zVQ2Xa)5E=ahv1jw5z`d})@v(PkN>78KjEBS?yi8EknU{Rl}|HGXqL{g}avNnm;wy2@!ZZZ|?bK)sJcOALVtuO?a(W<3vA6;GG@3 zr;wA=9KnjOD#x`J{!m{1V)Al}?Ngp>oq2msS2T0iD|UZ57;&sr!aU@r)r*~h zda6?$?Zzc0v@dtR+IVkq$sY|ZCXdgsDZj(d^Sf=cz6nSf9=;~|{k2HU!BcT=V*3-D z6TB0CDww5ib-dD?u6~$v{!jOqiq3+!;^iZim9?LPa?7^!N!b@k{q@;qPlS||2$;$Y8oLpW$?n7si&&Rpj72oF`vVmO=Z%|n98@4gob}PO;#YiGMDT^2@i=jc@I3pSA4B z$3gCs1KO{1Rz8=1cXW)7>zeY4uDb3>%;n)#IAxtzTV_6;qV!@u{k<~M9< zn)PnC=akoXXNqN)Kba?GcKz_KqSV#W1K&QoK3#Fa%yNr@@WBaDrmQO)C5H>F@VB2< zUw1*Ur`Yq#b+O;~uJeD5XTxtd>b*m++3|L9&aS9}=9;Y1&RplJ8y6b=&zL%f)LeAW zy(CvS=bTQ8{aBmlCaI=DVM6g9bLFgG8LG_qVf5Cnxwf9KCFp$tyhHGO&)-AEOKJyQ zDp_RJoR==XXsVK$=Y(0sxg*wlOOGAAHNE$>M|Pu;>4BwgR}Tez8d>3eeVLZlFq6Y# zMc+z94(plo8J@ys+M-!=*|uzEVhp4mo5?0=&t~3|oXTb<$3i-=nH?mjv6)XK9odZI z63FRnrjXB?p* zNxHF_A@PuN*~~(c^Vm#1Nq06gVmYJ-n~5Rm$z~c!da)U`6_DO+W(6e2hrgkoq&}Og zHvXZ|p4mE^7S1~}dB@oKpLFNM+|ZrbZZ@&x?dGBAp1##DYWVP#VPml!k6Ffv*eXZW*2Y8h(->BH5zkP=DpY8Mc4ad&8b42`oz%gy@ zFwrgg7k)|FJ(zKHb@YMO8S7&=*y(;qF)(|3GsflJ-EW2!oWJbSp=Ac`kzR%EPmf+~ zUw*+d6nl;ToRxn3hTq<-`y{!_D8}oD>(~W4Ymc_J z4(C+Nn_F;a)s}i&ljMleoVeo!Nm_mZ(`$3z&2TAVWK(kQ7>GEZ?pU(A{O3LO))RyI zZ^+ZTEqI5Z`h#wR)#lT6FW0^K*5om4!_4)wzK>lrc0iEB9^K;TH_xVrUpsQnHoZbx z=GPgwJqA~o#a5`U60PU_I+TB7sPM<9{XYWiznBf=H!OYgd`OqTgU&RqWAA#yFaDAU z++J0D*Vra+hP3pcbJrVfG_8{t%bryjX1rCzJ-T4M*pj*HPoMYotoqRWrB^4K|D+7! zw|lKzM)>3t&YK;Rb;73ac>T7~Ciz#-OSAU#Zwr%hnyZqd8fBHQeE9oZ)PHl=L_ur2 zli>#|7Olz&R^K@>W#cf3G5nkHV1C1`_dcsnJe>JJ&2UHey;+N-R@w;M-WT`UUqQuo zq2ZF|j|M4wMAkKmP0%^^p-r6gEk?Q6D=P58!Hs7I{8AK486-Z6ukS+m4ZBEGZ@W{H z@M$fpDXZHk%)s<>T+8d)TaO(s{B8A;Z?NzT-4|Q7#{7`?{ejI}8ix&XsNZv^Cedr} z=2x0wl9L1YZ`y_O?9y5*SlZ6|6eoRm&(%X>-xc!mxBm*$)9JOY%-wY}L~827>w$qU z?+clBNfb5iJ-2y+(}{0Cw(Yw+U#v8hl|NQ|om2W&K8F|Z43pj7tn>*>dZwn_*_8NL ze*e*;w{L<+y9|r$5oq6H`?md(-n#o@oO|BW2QD6xUt{K=`)Z2JM){l_Kcp<>yk;ES zF}Wt5&u|#OVTtU04;^Y7p7tuSlTK}oE*Q2ZWw*y|$MQzjS?@98Q!WNetcsW%T(Bv> za?_=z=UNrN@eM)UoA=)YG!JQiRjQKtXO|e~9PKa@ZH z8Irdza^jXLD>v5N+!*R=`q@)WRQLRy$!9p6->{{IX-n-<^ArW=@m`e~S0`;(&*}R9%)sEj8Y@JWa{K=s{&ko)2fQR?e5L zOUFNs_-t}bGl0_q zd2X(EMn1~=u}`E$!w>V!mYP>W$N5ImIOxJJuV& zyOws9&u|pK;ld}fyOvZZ2nQvmj8c?((YAn>P1OG(1rX z>s;A$V{Q!VhIWV7hi(G{kH2T{){VczXE&PPuC_s}g)>J`swd&07$J?$ssTa?dx^{e7p#733`M-RO;WwPT z;&SHK@Ps>MXN&hgG;(LkKi4X#$V{%e9Wr=N!XC*Dnp3l7g#(V|Yn`*X#(DFsQ>eMF zc$0a@sThkX(b@*Z)6*q9w$ob3KMoM}w=~UNvvi?J{@iPu% zX*9`RY`^SqpIl8i9RXI~LSET;O zF6zIG-*Dp(>8tW7W2R)aZJCG1OuS%8nYdkF{O0wyK}$`1Jb%gG8z(ws{Z%OroB?mWBx0&<@^r2 zuCw{6c|LXI^$4kvoi7*fvG1}<;~2DWQQW=$lk!uc72`G5FIgog{ULnHP_;d3N~cV> zI+)Fg7QApMS?tU0T44_V?<06G#Uif-J=H%ZI+UvVSjDeRkBPP!z`3lEC4Zo#dw|ff znr{JZURuq58IQtRm-c>JT7ABN8Jb}n=eYl~`hb}IWv4DSz2?7tv6A26&S!6*dT;-J za`OJ;pQ}c2t{hHE50^Gci8wd=l=%AcZ#P?`RsSkIt%;qxEzc_A%x&NLNl!N$KEAFY zzszXiLPzZhL-?9_6~Eztvm*vfZx~y4W8k!&4LNNFIXf~EZ?9}CviaGbQa7VvC+B+H zhn!`rtR$Nkh8%xk9q;iy{D}rf`pNE)a|*RqD<=f-8D7nAm?bxPUqlaQg7?|G%9iP; zd)Dp9E0M_^mhxbW;lbge5kph#cK*ui`4Gf-2UhpoUHhs!vUb0%pK936!N;Q4|B~wB zvzx$gH|5UKa~#iyi>s^Sr3*(Gc*XaMh97u)@9hi4-B(q5n*uit4s{*oGhIr^$Ns+g zgQ3g6c+Z>|D{}5gn~By^jeHAtA3nnze#7fF{$4!2P)9puQh3UwnZf54gfy*dD|>m< zsKxY7XT|mONekTOtQ`BzEuhC$&3XQk`#u92PP_e)iJf)o@T82x$6M$RH+XY=BAXev z64wiB*h~`1BsSARaxI%NSOuBPW;T#a!F3VIb+|5C4VjATB9dviE+V-e*F_1C8*p7j zawD#bNT%buhy$5{>mrhwxGsXkkK2!~Va;P@F)Rr7W?UsDLYvK$BtpA|@u0{YMl=Z` zm+>dDl{rI#VWihWY-7SmY-i4q*ulsrL+oUtN#rqCNbF)%Qy}u0j}RQ*+uHih zt974$Yt2uZ*O>RE-N*M%chl%$4X&xenUh35WtSw>bf=tEIQ1xbn4_Go(ZF=UhAqo~ z{;JtxuHG&$yQtPkZ@}KseSOpa_fX~hoxbm^-WwsE(QOjH!`mh-EjkfsHTX{M>5tKP zfKB7Kvtl#z_DD^z{+Y2*X-b^Z^Wy1ajXtM`HXR=l;bnI5lE4_z@BO!G1VrfXp$_pI zo*+{0@ltG%?!dp5e-E*41dMxq=zP(r$u{oKpM)uPFK?Y0we5rXe&08u8B5h-k9Hbr z+aFDTd8}Zm(c??8;o-d<{nuFi9v|j6{4AikOy>z(Q0Cb^`P-v2|9F2c7JgY|Zm~VW z@pe+i^1rv+QrU5{Eo7dJ^cIUYyI*)S=h@S5o$-2m?+S_RFVgF8iGGKV@Y{_n*;;k; zP`iZ9<3khc54$j&A<02DgWe=ry@{U5eiiRmt7-PaXqe&9Ujt^0aw*y%RP*9?iPrbLM4bWjHwt-`H%}mRqpIqx+#v2G?OSe1hNb(I{{4wd+ntTKSxN zyJNYk_C}*3owfU4yiiWmIGmFDyI0esak+d$x%$_raED}0k(7~DbimJ)v3n)T4$E$9 zIg!MFr+{8n$U6jWijz)!dGz(A=i`^W$iGjc_NxVP3^ANqWE{Gr+9XqU|sOiqsPI??}m0)g{diQs*)Dn zV6OY+QFnUH-o>NW&KTd)y}tIig<`wGEl2UEm48*7s#0n`_-@Gl-Wyb*c2!5FhW|_C z3g%1(Yo5nxo_#WJbSZtIa&O16WCT}xKL})c0z$M0f%wc^dovN*9-@7amzYXqtM`j}V7ZSm- zFU=gp*p#rQh{r^RhcAs15U|4wrz9)ZY-Mr!P5NzRiO7oMl|az<SL%Np`R#MhH*Z~dfn$scn*9q9*x2);EQmdHPMgb`rcTX_=`EZowcYh zo%arwh-f?>{EFA9B?@jiH%pgE+{8-mi>pJNP@^s!r}}7-PFw|zf+{60))NqrCPz!z zq-V)8O8KlQtRTiepS4zLAW~t@N(>AS(^(jczaBLQyoga$U`d<$Agfb{9hD?k7fulnZe=dya%E1#kp=0-ik}IzU z2xYIJE=E%ul3r-hO7s~s^Ee)afTnx!pZ~)9oSbwLPTD4;2dT1=eZ{jSGa1L@tcmQj zOlIA2R-Eedto~LF2#fend!5Pd*VRKR_Ky;|E&Zx=CigqXpZ@B{{1g^|NoEtBj^8bjsFj{hO1Z> zclmBO%ZO^QEjfRC`wyIx0#2ARY0Be0&zkp{W>TyLeTOqQ4ZS!9vu)qu0v&y(6I;4p zCE&(QCxp*Kxw^Sr-9UUE*019T%!3mD`Uj=pc8O3qZa6-01mosOhHWR|9NLQ-SQ1;&()=J zb=cO>fssMu5f%4O-F}0NP*DIH5sHI*uX!qhIaOv4yKN zLYRI#MFS;=t20KJelJT$E_C=OU;^a0I)DZ)X3#?yA_`UWb(*pTA< z!*PV0a3aEcNP*)hS7(l}YM(|x;22kjZCqd9lbWt`$Oj8R{Y(eVaWpI}0rfK-v`eRS zRv-+KG-%II|C7=h7(ht_s*;<~24O?8f}@J7n~X5MfS8W6T-_9esi{XnR&#Z>2y0-# zkwZ1mAt+!6IuNGg95di=;Ogc=mxnO5&_k|n9>Vn2LTZf`uFf4{a*%5Nh^zCU@kdQawS3G~ zdLpcZgjCBXT%8xfbg4?Zr(B&k!pdCTGp^1DI#sUjIalWkof=oy3LTAw`J_WDxKRJU z;3o7#*ch=?^OszmKf>fS)%+D#7l1GgQaWC9b%6-)MwnXT4ObV0FtsYR1`Rv}>6Ghh z8FK0^bdID90rYZsKgf67grNviX3EgX)h*!aDE)h`uA5E+90x8lHAZYTCQFOGz>?lh zTnEkpTGMF_ZvYp-ML=&5IStN$N>ByPf@)AB$RugACvyhEAz?u4x+o9>gTP=Q4kUmi zkOIEz`8LMuBJ$188NWm5^3G`fGGr`{@stKLh%vg}!$>0ZxJ=U_ICXHiC3O%Y7!; z1hN4A<~N(ggp6lPaOnGqG!Oyk_x4dB8pMEDKsW6}KqxQJ<>CT$OB0bCDiQR6yr4%CD5paEP2ad2W8hzHBT3a}E; zR-+ekFr*vs0#1Nqi2t4;q8Z!+v>kW=PJ=UmwgRP~4CJAJBFH_U0Ac##WhbB?A)dfz z1~L)&g89G?xB^=JCjm=f1*`$B^ut&Jo&v)WkOMzp;3xP6euF=t7YHCu9MIovNCGJ! z4d~06Rz$x5&%k|f2iyhC;2smE&sOG~MDP@#S%zknW8gU019pM=fTlj0)|7!NPy^~< z6wm;gU>$mKHK6ym2Z9E)nx>}3*>{{U^18j?7>tp3)lb!pa|Y0 z9~<%`r20VHV{h$Ao<38|VOK zK+7hrl5LK~zgn|Vi3@ikTKsZ9k?$o40)=1!3ZjdSh2S|pQ(OSTw7s7$i2gUj2f6?;0z9L10YTRRwC|_Orj39$ zt5?BgFae)iQCK@@0ms2KK-)GSFb~)PLtq5t!FS|K0qa02NCWHXF2)A15u}3*=7&Dp zoRfs0ColuXU<{xO-*G?-j0WzoNtnWKIY4D=!f)E8>jK(3UIrII9@qt%kWU@yw77Z6 zXI{OO5FQDX>4~i>2&e)z;K@Y|g-}M?C{w|->7_y`o($_ljs?0v8&E-`fChz;m-e}o zmafY70J25-2?`Sr$YTK{R(agFsig}iflNTBav6AyM7ux^*aETujXPgJ^P&|aZOm5S zP$yjiG~Mk0+d&3U07%uhS(1m81H%E8Dhq~zpKqFu=2myh>5BLHa z9ZG<1Co1+yPXC=n-v2aQG=MswZJ{bq0m?uPi~^d#2aq#72dKcwz!HoH+JJ^2&+h-0 zG&ISMwmbf_0F!_@m?paX1xHLwC6fNaiVhD~5k z;n*W+2W-I1Y9t0I^^h zhyyFAcj*HeUe1*i5f9dZ6p#$of+Ua#62NM(3Si0+SOYeK^&l0bfem0AU_cJo1Tw)E zkPSA2ERahUwjyvERDj(eAM64+(;E8sb30@O0%SMgsXxCX8=9%k%7p>+tZX3m+h&BuN~unW8c9pDXk3pxR{+j~Iz zw~WR_b|6a;&ogH)!mm`mOvJW$5(^_1&}l%IbTosT0t4U=9ZhW3kV^oan>2YTK+@#( z8j=d7iEuoyLYNj=nh02}dkdf9him(LqnZnk9HhSam znXpB83aCbypbeoN_cJX+-oOjctm^@2)~2P2t~+SmbOm#O3!wGWiGC+C8v)u;(k5vJ zm<}8PtrZS{cAT`r&j(a!5M&_m2Yy@`02u;;K`00VGy-HG!@(l3kSl4CUkR3j7!Ll6 z1xrCDSP$Yr6j%b%0HsL)5rCJL7az&hMMIJ8I%u7S%a43=r;u%_o zFcrd|h|;X!>M2Y$;KlRu^M6i7JlQ0tR|6`PZ1U1lS$(yqm6syOd;VHeVZ4^%Wun?7 zL7xa{w6B3&2U57YWIp{`u8z`Bm~8MI;I#yO&ZPe56`T$QugS?n@_r-O04Q+=SCTPu zf(&f}{59v z&nT@iS5kMW0lG!RyIr&w;Q~Nc7IYhlHo>$RE&_D@&j7lGlmqBiC5P@=(r%xcD;<(9 zN0I^El1v0Xz#IHQ4Q4`8YmfuB2u}fT5FQ2SM!_V|iSR9??SQ25@)o=UKfrhJ4e;XM zQ~!T~;sf}BOv52%K@a#0?m+hm@+0U1-GI`Po{W4&_#PzhGZiWfCzQY+gnxox;5RUb zj_zm-29(AOn9^+#y0#Gmf7!S088qfmL zU^o~D#)6Sx4A21TKozLaZ9iq807d|LAO~cD1o(?WcpeMkGda-<9XUYGk<(N=ey7M8 zUWF)*(usgUfR~nlC~igTWF&d1!42g^UG}AOeJe#b6-_2aC92k}=f( z(FjBVYT|gvWgrgFM6(i-CY=Px)qrZb7BYz&UJscHQouTp1~vkkoM>v^g79XL1$INX z3G#om6xE*JC}qlqj%r7l=<`;{H6Rh>^64_U;a!k zQGQN7{-ge}h1?DHfI{%U6OuvF?T6e8iU2JrR3I55qx%3k!qZV6vd#O$Y^Dkx*V`0mdS6Gd#a!Al?uKlufxw|nE80$>X!7YVcCK?IwhX5NX-8rOFL^@ad zLf0qVzH`ZtK6KcrFtwEKHC~v&He~%|M%%I17!O3Xq>%i}v`toP3|&Vcxq%K`>R*w? z5R)<@qcn2R&-#CcRG24r>`>#e$T}DaPR>;MTd@DiVK&PbHPJz7aom{YewB_6mO*n7 zL$!n(Nldvte8^%h*~5qJ%rATPOcUDeP)S{ft5x3#e6VM;kXFY8F4Hz*5Mu7`v3hIM z`r8|Z3=jqHzOa~tscbm~7J7pGZ*to)rgq`2@kn9XpF)~BIu%7}GFKrK=&TY)I+OZZ z#Ye(FlaNkd$A~PAWAqoWWrPj;`U!so62=}j#}H*t7|>t6rH$U-l@7#Qd zl2!gC;QhM2KK^;t)fs=5{)?H(G|fU*Ph^!s*67XWV>Goxmk(vpWeXjvxkfkZdS+;z zv@jdWS_q8{(xx$zGvL<3*r3S8NLae&^{LJGLXH1r-OTtPfyo}Er-mOm{nGBTmJch3 z{>Sf;P*aWWT6`%tV1Ir>&1Qx=8475NF@I;Gb=zjLN0^u)8MUrde#(}{HMhn4lTAPq zt>?zYmg@c(Hvk*O!x*dIQ z#OqJP|K1(EAE-;ITTEFd{J8>!>FF3?crtgK*t2E+kA`B5o!P4F72=G$Gdq!8C(eXN zvE|q(yTzGpz+NxGNVs5V6r%}femmLQ`AKXJa?q)cX+9JCPk9OER(_74E9=AxKe zi;mHxN%Iq0X!G#CTmFeppVsawHK;Zd#6*iO!Sqywn4z$KS=foOf3vv$UrYsJU@+0e z@}`!elJx$6F-_b&azB6SyEK{vb7MF#*NQ0Ixk=1d(5t-D5$O3Z(^tgM>7X}x$NY+c zlI(vmqFCLiAo1b09r=giF8zy9MhrPuFVHnrvx9B@FUAltLl6^oX3_zpfs0>J45zdq0~OiC&p@Y-oKb6#K7~!TLn@| z4P!cH|BKnqEoa=2F@~&-?FE9I|1@ej*Ic+?Veh@oXKemuI?v4%Ap2WXQhDgte=+wF zLp6H6b;+j!tFKl6Vmc7hH<1XIEtoGoG5%l7Z?3g1=AG&bGdMfa{zXY)P?5QFFE<~M z89w#fzZf;d^!51HMCDb*Zd?Avm>`A<5|OslI?c+t_Ah1{*P6+UYo`jbPptSCvpAM! zKkSH3G%%%3Y)M6$!DFMA3(T3jG$XzB#wnU>`UfHZx}f;qC#zMeOhN=(7O&kbpUGC} zTSZv?(=XdwjY*uvHsF3V`R^0CG)`EYdWi$(%YVKrS!U69CiHC|RMZ*8**ItVC%GAH zEp;Y;HqJ6FfAA(k8)=lvDz39clOMoC$;(u7wed( zWVI*#Cl$fmb!V$E5+3XkPF482gFH}XxA{7Ebwwe%fA=a7yC7^)82VqSI=oBE)pjjn zFx4CBOz8W%k=C=_8j0FkDZv3!tOi(_2~GNV=D7#v%{}A$-9F)xedtYOOc)i~KUB&P zgJ~}@bB~8o#4x8g#OPzsiyEID&y*oATV0p&g*1-W?Oz+`Ip3Y>^XW=!U&_9=30x4l zC@jc7CbVvYkAjtMWuJ9pYVb3Rj3;|0tDf=q#6IyRvl2p47Rwz~QS*4`!2#Cq9QzCz z)22$ufH~rcAtPnLyzpd);I+lmy^!G#raSVnR(ofQxtm;n1_QD$j=!ApLIycQ<^-gI zFLY$u&9AffXO{n0m_|A>-ORAOvEnKiF^b;U!7OHsy>T@$%D8_gAhxTuC0V%LurIwn zr7vODdc*Vg%q?VNEo9z7D4fEEnFhPLvV-r6Y{%(+p7(7_lNeg;(<^MUaYba>_nmHHQ2MF4VUP2>`pWNX)BYy=zbmAtgI($kCfb)BYVyFW zzsB*3VK-aTd_Tbg>Pxm;5ku!$;{8j>S~Du{^*P%&G_i_F%tz01S2Mg+$!0#<2CeQr zALo1$vuQq@?`Fi;VE{j#&mLj?c~XB5$PXD3?y4?%ms<)($pBn$QH$4&&=^=X-|sm$ z20d2AX!)Uu2O|Nwks{?C9-hp~>vw}ZoXc$S!>lkL2}U4+re*($k$*7zo$RpNDM*Mt z2d$ijOsgOKtF>f)`(XlULqZx3-+!)LQ170D-ThT)XzBQ5$>{mBo-!+YN?Evz5#NlbJU7RDUZHF`Wj^tVrWv(7yI~boc&NiHfsztG<|QyxkWXY zrCVPp-*|o~@^B}tT3Dsavp?aiBT;I$EqyWiIzng{xN;4y;8_IyBg~VP_sIU+ZaSZ7 zu0j9UF}?nnjGV0deJ_jDKM~#)n2!Y9Dd!Wc(`l`w)~a47s&)o3*j-arGiGK0_OWR; z{RwWlAG|rOXZOp#%N$&C;WGdK?XpS#B^w$sF93bdZ7O!H6>}4xP3k81H<`yp6PLG+ zKRQtu+MebCJ2*(y?+o&Edoroc7BOasLBDWq{I>}gdHW_rLxnR4QmfySFpC&6Z2B~q z&_|=gx;3*q5GU4b+y0tdDUETPT_PfZ1V%V-k+Su}5u} zP~Ow`WBM{-&ci*b|2o6T;azI%nTn1M{ccX&$?{lY|A3|oss?)GiUT7Rf{Ey^1LGTl z=H*G~6LRtS(KIGIgdL*b`}F8)rY?KH^U}jm!Q<6=<8m{0&8^JXt@B$=bsn zCJ0CSF2(8Y04~Di@ixTZ1#B($gIP@W0=AJbT)_e(#pov?ZUi%(L|@7^iZ7h{9T(FX z%5j)BXH{QexD9~2c}`4z7)JVUq@ZgjyYqp8O`|`@AO+nfKngMDRv4x#d*}X!KO9!t zd`+fCmRliA!ET6=M%EWkoG*pFSMu$P>C1}Y8nF=l9>NqX?4JV8E<{=X{nSuqJ}kt% zeT-SNl|9tl01YDW*R_AMHTdAon-c}|7QlMn!m0OvohG=_p+@-a#H24me~Qgz{-4&q z13b#&`9Im0UIK(b0t841MS)9C=nzl=kt&FYid>RQa^!Ls?kKZWZ3nXU9RZZKSuB+@yha}Q4}Y2u z1)1PD{(}WY%}*PQkX^IJB7V=(kKa2zEp5&l@BMR~4?VdPeUMcE&lD&+oH#HlNq!~a zvCv`F-C^xi;Dmy&qvxJq`_J$XH%SQ~(-3ci*YXkXWrI4^8YI=J+9O?0R_kz?5(3D5 zCBvLF8pIddSVUj_s3!-wxVtdE$-cMxH>)=c##7>^22BKnqP=r{M=8CYT0k+Xpqd)A zY7jpPs%-G!dH4wy5~@w6^Y?*#z7;aZ92}C|dnW9m^C{sWJMwtQeK+F)s676T$v6Uu(92^ zDeuoAYfK4yEO}EdAL0OwiMbpxO$1wz%g4Wn-;KFEst~{Xa`_ij{x+ANM!VF@phO1j zxuAEd-Ml$-f`?KCi#!x?4C>X~+H}WQ4yf*S zJuNdVeElf29BbiM(2~u!@C{D<`g7w)k=;bQ31006$5KyP zJ9SDT+Ygu8*&uAuRl~`b$9X1Clan@CnmjcM+9c_znmTF!L9UuKG_}#`SxhsJ9gVLI zKX2#bMzbt-!_Lo*#+E6|5$v9wCy&7rdfqNwpd$-%JLF8c^$JF(x{CJzLCR@-TH^`U z;BK2kRS5PwZJhqS`|FVgSC`TQjPr_Du z$bi`(m5azv(4r>B^KS1y4Bs>w_=jg6H9pmG@#W*NNYueZJ~=|hdW-kwck5=K>NWj( zisnBE{f@P1^c_rTE1=mCeWmvoy{el3fqTt@>{IQZHpza=;yzQND(8I!?LW@;!}-q> zSkGXexI))5`*HXH>g2x=&lmE(6Jd{U79!k%D&%c@;>>yH@i|Zh$^*h1Uow)9nE?g7 zIFi3P5g+e9QN%Az#0E5$lV6tl%_wOL9=%>|^V$PDNEvX1OyTHnfK&&`8^?ym%y>1Z znS_vse{&SKzJyLI;zAm=`0=mR4J(F3P%aF(vmH-zPm z<{c-og((3dML5p1w>_)Ikln`AT$VoM%-iTfjUh$Krk9$$jfhMdC(knu-?@^qtjz_d zK`S*#@H+KnIA_;LAkQ#uD zI@z%L;S56&AmpH@;(Fr*ei+nQ?-%F2%4(NM4N`Mzb#&t%er5&>i%G=6I7D!!iDdu$ z>M7NZA1_DQAvLw6(uoX^$)S)E14MAY80WndCT)2Yxz9Gp22na7eR$&zX_c!{{DyFZ zCTTKPrm&<`dY^zAtc@PD{MMv<&Hn<@2ge0uQ{J<6{UmQETOXO7)g zkew5hIwRzVNx#&)$bv_rtyqMr@zmyN{S425IhhMJ`4UYh(Ls%8OKw*NH#_v<(kN03 z;ud-OF0=WDMHpAcY~FJwe#gw_);IAxV>Ukh1>TkdGYYcn+4OpcrA?oi22AKZjdLd; zbWU5c>v84ft%=cqkg8!leLtIjL9{M{#$rg~4q8&K@4}X;H7)(d9uqW#tg6i6cd7Tt zIs9YlwcQ-vcNQ>u&XG#;ap(&n;egYE|0-qm+SvK|EXrl4 zAk6^L=2}bzc0iu)@Zz-Rw>@#tjU5#6n(Xm*mq{}$ZAbqtdK9A5PpK7V6q+-gIY9>p z#Z<17QMNa5$Zqfw?3J!o5KX2j%UED>I@T;WR;}sw_xq|1z(|J0W;KD4@Kw${Q~RTL zR0w7&bgo>`!*aU)Q+T?a*e9WRz-bBy38iRDmG8be&$T3L3ho30QUj1iPfrXlcq5+D z$Q6kbYz1n%JuDEf{0nThF63XJJNT|(ff)=%G<(VT)zU;_Rn49`K%CV3K3UOb@lEBIC$2fOyoLw2D513#;|hB8TJW`BwmT^; zsO}_L{Up^6*+(-AS@esw1Hz49d=_tk3}h9dPsw zaH4|Uot9~UtxIJ|=iCH1DpgmYdBjXZeCxPmy3>$rBW^vigid!!2cS}$0jYuEbl=;d zc5i#{rv*fC@+lzXzf9dfuJSjZpZZrqXcsL3q$VIwKN}qR(WzoOR3&OIu##hR*a{)j z?wzJqe6`1E+ECPa%9<}X(nSjn)*YU=wX#4Wfd*X%Bm(3f-FLf9r$fW1sN}$Ql?Bq% zx;p+)-{wtkE>t1V_hx{=IT>$T@Y4B}4R(riRTp?@r8`zYIsmCUkPfbXrP{Y;yU|4@ z;u{_H0)*7-XzHcl-iK=S@Ica4$afQWMb!S{56WZ^4u)C?NGNb3mW*C7ef5Au=5m8x z1&DN{Nw4Uodpmt!Qy>YaXbvD`TVvbrF`b{%f1rolGL>9N+Pn+(M)w~C2uTiOeP6}7 zocZ%_HztqnDR6{0vm20bP#evVe)W4G68~#Cu!n_bxj{nis)Bwkrbo2+zB?5;A z)Z)o)OZqk)MRtrP0@KJX|0p64En( zklCo-^YFm-_J>|o$zen$KuAlzf4cR$8HR17CF(?&9R|B8+cdJc>x#wuTc$`Y5edQq z;M8^jDPq$81=|O`^OH&rW1R{RNg?d3X^(7w>(FHt0v6^1LT5i~z8GATN-jwd zY4<)Lk?2x;j%pFRnA)1IJT%ND6NG_w**G9fFXhC$b0O8s$LIgl)jpwJ0S-aEK zE<{CYjn@kRsS8L%(_f=c%~}}af%E|+6p$}36n^_t-9B`WmpTFqMnFjK!#byQDcF64 zY%m}$8tX_EG-2hQ(C^AjZ|^~xsgf%j>fE|?QCH2Fq~fwTAT*+@RlM&s~eQJo%M?K;S8X43JCJM;&8}8PBrW4JKnoPEe!YCQaRbbRNi&p9C$@Utm=h zOXm7#gdA_Nc!-p#kb?gza;bcST)@>2cNTASVvPqjP5K5}XCXt{J}kH%WF^CJ23(Ya zxl$%o&Ny%$gl(7q#u88p!Um?iDR;1|#gk_(sPKyn`-DlI2S^xZ{Oib_-}nDu?@D#X zVN3o`7F^>??``@l=+C&9N%ga+`tqTET!HPqK9c^3^qIT1L@w8lMqcRmZPz=URGq^V zt#fgyTBV(g^-HB`3TktG#PV)aN2^F!$vz6?qd(h#LtApK86%_bp7{Kf2PbNo?B{}U zec2Aqx4u>(39x?;14Qe!^4WG5o_KAxY948~ZVO0tK$NEnr<|J?DdJeUN9Y)6xXwM) zj+QQ{l-h9HwBOZ^u!;h*?Gmd}XT5?2eTa_5fPD0EO3TzG%p+E%4!Qr)0O_oLNWpId zb9wDx`f8$QQj-Ij)c<5rT@PwfA9zGw19||Ny8eSz|NqN^T*$6K4yWH7JC{<*Ko2!sS2*aiJ$Weyj)aeLg)fcM1 z6vn?TjrxYl-{p;*wB<{fVd@9!=&M<-2ivVrf*kGe_Pc9u9X~s*iLmM-iK3&9kg3jq zFjTJrxx52taHlW2|E%5mEMW4qCGIL6Na*klayl~(w)H1MCpg1$&02%4isGNDwf`A> zb=qkyOorQ}!vgr$??{I^q6WJ_F;nUIXER6tH30I&du{?xSi=f0}-;N(=vJvehNpEQDgR?DAnwIg3+;;ki+oSv2>c`2&P zE7>Gs8J*dK=cc7hN@q+&CIhR;FOh+1)n{9!<{Dp`+_?2nFOd83TuXE9<2PT!Sd(yy z2NtFPLV>8^-_rh5Db*4<(Wt3MNrcGpT0@A8ne->0iyRkA-WUIc*i-o{u=p^bSbt!Y2L%`emM&* zC`*Kq4dlb;;DX^dB4#`W)7?-rP;{`bYh}7fhEOE`;Rbwj(q} z+(%Agzii^OUc>cFc|DooKJ!w}4E&GRupV2z&#!PC+clqy*A>Tpz=zJo83$g)7&RAX z>3#XTbHRz{T641NIa~xKPblu~jyumyN+-*T`XvwmSqNrv{)%fJj?*g|_?ag`RT` z0R&={_tMMMCj}i>$rT%vRn?B{xdt3HmHrbTq?K8x_Pn}j(;7NIR9T4IDl>1EDjyZR z*Zt^0ucWU2iH6-|%rxZZX7BED`NC&EZKdoe-Tu>KTU-Fo-X_n@uZ?*(ZAU-+n}H-b zu!k>8_{g-qw@?R-m)zx~yzo|B%ge8ej`Zb|%1`48uq*UKIem8ZUmJQTVbzg(=L`M@ zHs+qt8=(ayzy>$ll{#V80D0mk5}OHt(5c~~t2Ni|`F;Tf)FdfP2)ChT8zC1xVZXdOrVaug)&ILZO19K9U`k?_`E!quS5vyCP^OoyWC zQp=^YF(Z-(X807IeYf{OjJGkN)6cF5rQbIDC~j z@jgiE3h1*_uBU!K^>aO1aqBIRBMA70v5J2k7finDF8ROxO%L|ifG;Ma1>mHh^7V{F zma>~~LQAuCx1@C*X`xVsn+2N$Dfr$SQsBV zkCkKF_wvQ_U}U8k?x|T|D&zH&7JMvc5|J~00y_vb#7W179bai!4-ACvau{DOoH@@Wh3+PT)vcMJUbS5N-623mKi zGxy6`Svm8I-S4oOGQ}w*905h66>u?c@1jA!_9I(LbC2CYJz|eM2Hcux@rUz|7PAMR z=}#M+Ff&0(2l#0m!q?k&KrYu!pZ*+K_tIuMts?Ef9_=j~9p3oi#qJ9aQi$5M2K73_ z16)B+<)IB%y8scnbhh3F2=#h{zn}y&k@_<6>xd`6;Wy9fAn((&-*pL%XWrmVTmn() z`>2N|hE9j&dmtY^-Tb`sPTNqAT4)NSLz)>6^GZu_kxSl(>8V|!a1HoeZrJO-tDUmz zM(>g`?$=$00YWD1n9hMM0U8RqZ&)jbXYs6t zfgfDX8gxGXh}=O3UanU1bGt38enFXBy|?NPm@q2-(e{6$Oq*|H_dVxl{$yx*3zWzN z_nExKo_%c2l4<Y0z9{1@e# zD6h(Wc4}6)&XG~{VRZc0%}|D~Sixe;8Zu2IM)Hys>@xpsC5x?;XR_lSjni&2@#wEu z{o=N(*q6Zy#>lI`&7M`@gL3QJ7}KVUcz1i}pLl^j@drH3?5@<2PpB);tF30Ce0PKr z!#7k>$`(Jqn*F5kHS1W%Cyg04yVX!=wB%ZB8Ac0vU3tcAbB4j;w41Hj`Q{8;hRtHL zJE)v*u;A@3yU{`~{imC(899YHMFu3*thP*(!Cqjs0)=;fm$ghtlkb7W;nM*0BwA>7 z1x~ZYk!H%w7C26Go+)lb9$)w_YtSgin4e!{z?W0Bw&`|Tp~Ga?iUnqVMo}8^`1VRw zUT!^W%6DvJ4eMo^v$6~h)9?cDWOCsBGssH}%4Fve@3Fq&X~R$%=g7!0hec7u*Q7Rdu%5EX%m~kpV`Ew@UTs+Igc%7-T0TqEV&H4 zoU{ENT4NL+ zc6V8Uhwb+wT7eJV&l;-G13+81Uqg#O0D6D#$IEt--2+S;%mG%BPdI>JZ(|Z9`Q%2H zz-JY+(BNc#<_L?b;D|HC| z@TnVFGJm|463R!s&)(rvKVWC%s+qQ#y~JCtXP@zuL##EwaDcVt)wi%oJns-o<&6)r z>AcC8SZ5^%!RoawEQQDafPb>+-a$ZD9Ae?UGwe2p!{Ep_7Fx4SPFhVSV_r@{p3!P> zn6s@$iv!C>)?$fcCCF!MejcqOL%z|OBik8FBh5};;V>J+e@uXi&i@=LdhiJA&xf7F zSUx?>w(?m=z}`!TSr7v4~D` z38)JOz1gwGhB<834E1@0BZHQ-&1%jru!C#0>@c%iJrRi zsb8`NJa{d{fVEVPzkiJN;74lUsbL8V3r;RhE@9<^`Jpdam*B+WkgwQ{ioprI>?zic zuQU`4;NbBnh><)ka0_(`fU1AlBJ6&Q^%RwlI zuCZPUHmBmqKiTE7!Ku8`P4+k6aEpE0G|iZopKrGfGi5mA>@XSm`7ms0=@~Xys@8@K z+XxuJY!mFSP^E)jg~bGBSPC*tn`BQf!r9|;+!Af~5mgl{L zVg2ebr9POR$dtYkKK~vYz!%KRe*w6nqI3zyi~WicSFDs#vVwWnT`U}`6Z{FZT{B(v{40bl zE7hzVhc&C?(-!v37a10eU6XMj53A zFIvee7mqKinCfBv&qXQyH$18 z5Om}j?ao5GF`ukTWOaGZ7fsdMm%MKcrAbZgb}#C+N51m}Yx1zT+$)&GC0_%DRmp;= zJtF`w(LJ=jfZ^^JgTo$8SmFRK77Nm8a~9>BVzVqpXF4o*ZlTSdnGF`n8yuwI#_3}c?jVjg9(!@!#DCPSXlY&B$=EhZ}fkgy>g&K8mu z*@Zdgj2uIrEwcc=h_I<|{KX<`;xlLD#eyM0DZ}WH&~OiFAqTA=UgJ7z67FtG(jX29 z&yd15uEu6~>MvGBK~%~Ueqhmcz57LLuOjakqcpAO%_HF6MV{7KX~IKpunLWRhJ|K< zhRcm#u%`U@4Hg1jH68{h|NMfFJ*D#KD=emopGZx1P$^X>=DC)88qmc3%-_ApnpgtO z8d?RahJw(GD?+jsivj;&c_!4!49#&{VYrV5V>-5DV+PE(0UtcdGZa{{ALf&y^WiB< zgJ%A<)>a=UQ?OuLJRre(YCAtYlhQovkEr2@dS&E^sWJhZ;K%tSreBRcd}EB zR}3u?SKr)fY)CK2$}-sv=|ynAp?cZQ93c@|?WN=ni#fw&9APwDXn+P%{7i#r>c})Y zjfN3sQ=v*M)97+nc*uISqDos<78C}2`bxG4H72_qp#&a~dBfgnFgk5{R3rs++8}eR z2`6?|2TGU`bDr609%0g?Ks>wDl}Rg!94oVxkR0X=;bj%&rQ0m%*O7r>g+!Zev85wi z$uT=1d~J>qV5lv?j9Txs(St(}aDb@EPDadOu{q(V31LVPxa=q+P!h7jqkHUlwwkhy z#IFG>--a@1BRB7jO~`VTc<>T5N`C5!SRa9ft!(KVq|r@li~Icq=Nc)7WJG$7a^Bj$8Ru z(qiXv6ApK;7=Yc+h6KI~k$kvz_Z2Yh(SUm-f~ERUC{0NL_bO5E+<2NrClXv(9yD-2 zQ{VV@0qx4*SzCCj`4Br)*8l)e2v92qDr~K42*g@Z?JO@j%_5W3@ss=OvnWAO4gyDH6fOjRtXi>g1mHWHtw=qB+{orcv_~0u&n3=ZB-b#Zxp(gs{yO8Lc|UL z4S(S@yaRu-vW-tqUQK;jl5KtKIBlS{hMtK}D zH1l2LW?&`ETgzhVsMKg=h@ zY5(3n(CE7K}13m`OZx& zrbl4^0PO+;z!x2``Io_W2NVV);?v#k$l-c)+k0*0@ZVJH-m>LTq%%C;QfxXM&XdR>TIErryzBm zY!z0%zuzWX25{)yGujN4d>lZD;!{Ne5<{wo#%|A1Hgp%o3MFb@UMIyxel-R<%}?gT zk@rmWc2}vTcbOPecXTr{dKBebO%`F8>p8q?fahHlkrkiyH7iq7`pM8p|l zv<-dhfbCnsw{K!iT7V+ywU@IfyYZ`Z&tyyYyvabzZMOr}^Q&iAQ_sYEhi2%-4?|jV z=}>xsgkAP+EloA0NjeCf2^`@8=RQ!k9#f9AT!7e7Lk|EFJXRO4d!v>3)mG(Wcs(3- z_-hbvh11+nA=_j{3e{<%XOWse4=85@h;$I88{#8i*$Nj)! zYSQgBGEw3eUNP>i@iRABbd+~5Dqrqi!C|UMk%zmvQkw`44as~*H>GhyZ}wHVHX5wS zWU;=BvlEo*=Svbc{%qgCfK@MMsv!vcw#udjeCTAi<1l^oC1fqq~zur_pv zj@}ZZ#*B$dqCd&>CnX0-6S(%-X3B(Q*+xJgU@NfW;v7lS3QF%-)eK_R{aT>^`+?<- zLtHrWGe_bS#BPUTif7Zvd|ZZeP%cZX6*1FK1q7j!fc1j9SRpDn!z9&Q6xA^p%$XvT zYVKD_K7@ajB-mOwW{rqpoWrOjwbq^*7>=W@l$iR0h&nQ@m0E<}Q_eEZPvmIjSA*GV zjZfY{PDn4}8>^jgUVSu1Dc=)K)hmOvQBfmowi@iXM zHQZeULH9GZE;89QOZ5c+a!I)AQ&)-L%ZqUe8{Jx&qJ*X4oVo~CLE^+6UAxVO4VgcY ztTf{%+AELp4=>>=L1BAXuyO5_3jFtWij$x2pj6@OJ18N1b9<$+C>B55L5V4^K-k57 z9#!fF^P~@OIDGmsr6zy#F&t=WMT7gPChyW!sqHRSkp}WGt<}clpuEFueExRkG;Qt49mYk0O diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 000000000..8e3a10b0a --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[install.scopes] +silencelaboratories = { token = "$SILENCE_LABS_NPM_TOKEN", url = "https://registry.npmjs.org" } \ No newline at end of file diff --git a/examples/CREATE_AND_USE_A_BATCH_SESSION.md b/examples/CREATE_AND_USE_A_BATCH_SESSION.md index b62f6cbd8..7d528a1bb 100644 --- a/examples/CREATE_AND_USE_A_BATCH_SESSION.md +++ b/examples/CREATE_AND_USE_A_BATCH_SESSION.md @@ -95,8 +95,8 @@ const smartAccountWithSession = await createSessionSmartAccountClient( paymasterUrl, chainId, }, - smartAccountAddress // Storage client, full Session or smartAccount address if using default storage - true // if batching + "DEFAULT_STORE", // Storage client, full Session or smartAccount address if using default storage + "BATCHED" ); const transferTx: Transaction = { @@ -117,21 +117,12 @@ const nftMintTx: Transaction = { }; const txs = [nftMintTx, transferTx]; -const correspondingIndexes = [1, 0]; // The order of the txs from the sessionBatch - -const batchSessionParams = await getBatchSessionTxParams( - txs, - correspondingIndexes, - smartAccountAddress, // Storage client, full Session or smartAccount address if using default storage - chain -); +const leafIndexes = [1, 0]; // The order of the txs from the sessionBatch const { wait: sessionWait } = await smartAccountWithSession.sendTransaction( txs, - { - ...batchSessionParams, - ...withSponsorship, - } + withSponsorship, + { leafIndex: leafIndexes }, ); const { success } = await sessionWait(); diff --git a/examples/CREATE_AND_USE_A_SESSION.md b/examples/CREATE_AND_USE_A_SESSION.md index 738c6718f..5acb81082 100644 --- a/examples/CREATE_AND_USE_A_SESSION.md +++ b/examples/CREATE_AND_USE_A_SESSION.md @@ -17,6 +17,10 @@ import { createWalletClient, http, createPublicClient } from "viem"; import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; import { mainnet as chain } from "viem/chains"; +const withSponsorship = { + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, +}; + const account = privateKeyToAccount(generatePrivateKey()); const signer = createWalletClient({ account, chain, transport: http() }); const smartAccount = await createSmartAccountClient({ @@ -26,12 +30,6 @@ const smartAccount = await createSmartAccountClient({ }); // Retrieve bundler and pymaster urls from dashboard const smartAccountAddress = await smartAccount.getAccountAddress(); -// creates a store for the session, and saves the keys to it to be later retrieved -const { sessionKeyAddress, sessionStorageClient } = await createSessionKeyEOA( - smartAccount, - chain -); - /** * Rule * @@ -78,7 +76,7 @@ const rules: Rule = [ /** The policy is made up of a list of rules applied to the contract method with and interval */ const policy: Policy[] = [ { - /** The address of the sessionKey upon which the policy is to be imparted */ + /** The address of the sessionKey upon which the policy is to be imparted. Can be optional if creating from scratch */ sessionKeyAddress, /** The address of the contract to be included in the policy */ contractAddress: nftAddress, @@ -96,14 +94,9 @@ const policy: Policy[] = [ }, ]; -const { wait, session } = await createSession( - smartAccount, - policy, - sessionStorageClient, - { - paymasterServiceData: { mode: PaymasterMode.SPONSORED }, - } -); +const { wait, session } = await createSession(smartAccount, policy, null, { + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, +}); const { receipt: { transactionHash }, @@ -116,7 +109,7 @@ const smartAccountWithSession = await createSessionSmartAccountClient( paymasterUrl, chainId, }, - smartAccountAddress // Storage client, full Session or smartAccount address if using default storage + "DEFAULT_STORE" // Storage client, full Session or smartAccount address if using default storage ); const { wait: mintWait } = await smartAccountWithSession.sendTransaction( @@ -128,9 +121,8 @@ const { wait: mintWait } = await smartAccountWithSession.sendTransaction( args: [smartAccountAddress], }), }, - { - paymasterServiceData: { mode: PaymasterMode.SPONSORED }, - } + withSponsorship, + { leafIndex: "LAST_LEAF" }, ); const { success } = await mintWait(); diff --git a/package.json b/package.json index 37ecba983..71ff37f41 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "sideEffects": false, "name": "@biconomy/account", "author": "Biconomy", - "version": "4.5.2", + "version": "4.6.0", "description": "SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.", "keywords": [ "erc-7579", @@ -54,7 +54,7 @@ "format": "biome format . --write", "lint": "biome check .", "lint:fix": "bun run lint --apply", - "dev": "bun link && concurrently \"bun run esm:watch\" \"bun run cjs:watch\" \"bun run esm:watch:aliases\" \"bun run cjs:watch:aliases\"", + "dev": "bun link && concurrently \"bun run esm:watch\" \"bun run cjs:watch\" \"bun run types:watch\" \"bun run esm:watch:aliases\" \"bun run cjs:watch:aliases\" \"bun run types:watch:aliases\"", "build": "bun run clean && bun run build:cjs && bun run build:esm && bun run build:types", "clean": "rimraf ./dist/_esm ./dist/_cjs ./dist/_types ./dist/tsconfig", "test": "vitest dev -c ./tests/vitest.config.ts", @@ -72,14 +72,16 @@ "changeset:version": "changeset version && bun install --lockfile-only", "esm:watch": "tsc --project ./tsconfig/tsconfig.esm.json --watch", "cjs:watch": "tsc --project ./tsconfig/tsconfig.cjs.json --watch", + "types:watch": "tsc --project ./tsconfig/tsconfig.types.json --watch", "esm:watch:aliases": "tsc-alias -p ./tsconfig/tsconfig.esm.json --watch", "cjs:watch:aliases": "tsc-alias -p ./tsconfig/tsconfig.cjs.json --watch", + "types:watch:aliases": "tsc-alias -p ./tsconfig/tsconfig.types.json --watch", "build:cjs": "tsc --project ./tsconfig/tsconfig.cjs.json && tsc-alias -p ./tsconfig/tsconfig.cjs.json && echo > ./dist/_cjs/package.json '{\"type\":\"commonjs\"}'", "build:esm": "tsc --project ./tsconfig/tsconfig.esm.json && tsc-alias -p ./tsconfig/tsconfig.esm.json && echo > ./dist/_esm/package.json '{\"type\": \"module\",\"sideEffects\":false}'", "build:types": "tsc --project ./tsconfig/tsconfig.types.json && tsc-alias -p ./tsconfig/tsconfig.types.json" }, "devDependencies": { - "@biomejs/biome": "1.6.0", + "@biomejs/biome": "^1.8.3", "@changesets/cli": "^2.27.1", "@commitlint/cli": "^19.0.3", "@commitlint/config-conventional": "^19.0.3", @@ -91,6 +93,7 @@ "@types/bun": "latest", "@vitest/coverage-v8": "^1.3.1", "buffer": "^6.0.3", + "changeset": "^0.2.6", "concurrently": "^8.2.2", "dotenv": "^16.4.5", "ethers": "^6.12.0", @@ -117,6 +120,8 @@ "commit-msg": "npx --no -- commitlint --edit ${1}" }, "dependencies": { + "@noble/ed25519": "^2.1.0", + "@noble/secp256k1": "^2.1.0", "merkletreejs": "^0.3.11" } } diff --git a/src/account/BiconomySmartAccountV2.ts b/src/account/BiconomySmartAccountV2.ts index d419776ce..a0537019b 100644 --- a/src/account/BiconomySmartAccountV2.ts +++ b/src/account/BiconomySmartAccountV2.ts @@ -1,6 +1,7 @@ import { http, type Address, + type Chain, type GetContractReturnType, type Hex, type PublicClient, @@ -18,20 +19,26 @@ import { parseAbi, parseAbiParameters, toBytes, - toHex -} from "viem" -import type { IBundler } from "../bundler/IBundler.js" + toHex, +} from "viem"; +import type { Prettify } from "viem/chains"; +import type { IBundler } from "../bundler/IBundler.js"; import { Bundler, type UserOpResponse, - extractChainIdFromBundlerUrl -} from "../bundler/index.js" + extractChainIdFromBundlerUrl, +} from "../bundler/index.js"; import { BaseValidationModule, type ModuleInfo, type SendUserOpParams, - createECDSAOwnershipValidationModule -} from "../modules" + type SessionSearchParam, + type SessionType, + createECDSAOwnershipValidationModule, + getBatchSessionTxParams, + getDanSessionTxParams, + getSingleSessionTxParams, +} from "../modules"; import { BiconomyPaymaster, type FeeQuotesOrDataDto, @@ -40,8 +47,8 @@ import { type IPaymaster, Paymaster, PaymasterMode, - type SponsorUserOperationDto -} from "../paymaster" + type SponsorUserOperationDto, +} from "../paymaster"; import { type BigNumberish, Logger, @@ -49,12 +56,12 @@ import { type StateOverrideSet, type UserOperationStruct, convertSigner, - getChain -} from "./" -import { BaseSmartContractAccount } from "./BaseSmartContractAccount.js" -import { AccountResolverAbi } from "./abi/AccountResolver.js" -import { BiconomyFactoryAbi } from "./abi/Factory.js" -import { BiconomyAccountAbi } from "./abi/SmartAccount.js" + getChain, +} from "./"; +import { BaseSmartContractAccount } from "./BaseSmartContractAccount.js"; +import { AccountResolverAbi } from "./abi/AccountResolver.js"; +import { BiconomyFactoryAbi } from "./abi/Factory.js"; +import { BiconomyAccountAbi } from "./abi/SmartAccount.js"; import { ADDRESS_RESOLVER_ADDRESS, ADDRESS_ZERO, @@ -66,8 +73,8 @@ import { ERROR_MESSAGES, MAGIC_BYTES, NATIVE_TOKEN_ALIAS, - PROXY_CREATION_CODE -} from "./utils/Constants.js" + PROXY_CREATION_CODE, +} from "./utils/Constants.js"; import type { BalancePayload, BatchUserOperationCallData, @@ -83,55 +90,57 @@ import type { SupportedToken, Transaction, TransferOwnershipCompatibleModule, - WithdrawalRequest -} from "./utils/Types.js" + WithdrawalRequest, +} from "./utils/Types.js"; import { addressEquals, compareChainIds, convertToFactor, isNullOrUndefined, isValidRpcUrl, - packUserOp -} from "./utils/Utils.js" + packUserOp, +} from "./utils/Utils.js"; -type UserOperationKey = keyof UserOperationStruct +type UserOperationKey = keyof UserOperationStruct; export class BiconomySmartAccountV2 extends BaseSmartContractAccount { - private sessionData?: ModuleInfo + private sessionData?: ModuleInfo; - private SENTINEL_MODULE = "0x0000000000000000000000000000000000000001" + private sessionType: SessionType | null = null; - private index: number + private SENTINEL_MODULE = "0x0000000000000000000000000000000000000001"; - private chainId: number + private index: number; - private provider: PublicClient + private chainId: number; - paymaster?: IPaymaster + private provider: PublicClient; - bundler?: IBundler + paymaster?: IPaymaster; + + bundler?: IBundler; private accountContract?: GetContractReturnType< typeof BiconomyAccountAbi, PublicClient - > + >; - private defaultFallbackHandlerAddress: Hex + private defaultFallbackHandlerAddress: Hex; - private implementationAddress: Hex + private implementationAddress: Hex; - private scanForUpgradedAccountsFromV1!: boolean + private scanForUpgradedAccountsFromV1!: boolean; - private maxIndexForScan!: number + private maxIndexForScan!: number; // Validation module responsible for account deployment initCode. This acts as a default authorization module. - defaultValidationModule!: BaseValidationModule + defaultValidationModule!: BaseValidationModule; // Deployed Smart Account can have more than one module enabled. When sending a transaction activeValidationModule is used to prepare and validate userOp signature. - activeValidationModule!: BaseValidationModule + activeValidationModule!: BaseValidationModule; private constructor( - readonly biconomySmartAccountConfig: BiconomySmartAccountV2ConfigConstructorProps + readonly biconomySmartAccountConfig: BiconomySmartAccountV2ConfigConstructorProps, ) { super({ ...biconomySmartAccountConfig, @@ -149,53 +158,54 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { (biconomySmartAccountConfig.accountAddress as Hex) ?? undefined, factoryAddress: biconomySmartAccountConfig.factoryAddress ?? - DEFAULT_BICONOMY_FACTORY_ADDRESS - }) + DEFAULT_BICONOMY_FACTORY_ADDRESS, + }); - this.sessionData = biconomySmartAccountConfig.sessionData + this.sessionData = biconomySmartAccountConfig.sessionData; + this.sessionType = biconomySmartAccountConfig.sessionType ?? null; this.defaultValidationModule = - biconomySmartAccountConfig.defaultValidationModule + biconomySmartAccountConfig.defaultValidationModule; this.activeValidationModule = - biconomySmartAccountConfig.activeValidationModule + biconomySmartAccountConfig.activeValidationModule; - this.index = biconomySmartAccountConfig.index ?? 0 - this.chainId = biconomySmartAccountConfig.chainId - this.bundler = biconomySmartAccountConfig.bundler + this.index = biconomySmartAccountConfig.index ?? 0; + this.chainId = biconomySmartAccountConfig.chainId; + this.bundler = biconomySmartAccountConfig.bundler; this.implementationAddress = biconomySmartAccountConfig.implementationAddress ?? - (BICONOMY_IMPLEMENTATION_ADDRESSES_BY_VERSION.V2_0_0 as Hex) + (BICONOMY_IMPLEMENTATION_ADDRESSES_BY_VERSION.V2_0_0 as Hex); if (biconomySmartAccountConfig.paymasterUrl) { this.paymaster = new Paymaster({ - paymasterUrl: biconomySmartAccountConfig.paymasterUrl - }) + paymasterUrl: biconomySmartAccountConfig.paymasterUrl, + }); } else if (biconomySmartAccountConfig.biconomyPaymasterApiKey) { this.paymaster = new Paymaster({ - paymasterUrl: `https://paymaster.biconomy.io/api/v1/${biconomySmartAccountConfig.chainId}/${biconomySmartAccountConfig.biconomyPaymasterApiKey}` - }) + paymasterUrl: `https://paymaster.biconomy.io/api/v1/${biconomySmartAccountConfig.chainId}/${biconomySmartAccountConfig.biconomyPaymasterApiKey}`, + }); } else { - this.paymaster = biconomySmartAccountConfig.paymaster + this.paymaster = biconomySmartAccountConfig.paymaster; } - this.bundler = biconomySmartAccountConfig.bundler + this.bundler = biconomySmartAccountConfig.bundler; const defaultFallbackHandlerAddress = this.factoryAddress === DEFAULT_BICONOMY_FACTORY_ADDRESS ? DEFAULT_FALLBACK_HANDLER_ADDRESS - : biconomySmartAccountConfig.defaultFallbackHandler + : biconomySmartAccountConfig.defaultFallbackHandler; if (!defaultFallbackHandlerAddress) { - throw new Error("Default Fallback Handler address is not provided") + throw new Error("Default Fallback Handler address is not provided"); } - this.defaultFallbackHandlerAddress = defaultFallbackHandlerAddress + this.defaultFallbackHandlerAddress = defaultFallbackHandlerAddress; // Added bang operator to avoid null check as the constructor have these params as optional this.defaultValidationModule = // biome-ignore lint/style/noNonNullAssertion: - biconomySmartAccountConfig.defaultValidationModule! + biconomySmartAccountConfig.defaultValidationModule!; this.activeValidationModule = // biome-ignore lint/style/noNonNullAssertion: - biconomySmartAccountConfig.activeValidationModule! + biconomySmartAccountConfig.activeValidationModule!; this.provider = createPublicClient({ chain: @@ -204,14 +214,14 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { getChain(biconomySmartAccountConfig.chainId), transport: http( biconomySmartAccountConfig.rpcUrl || - getChain(biconomySmartAccountConfig.chainId).rpcUrls.default.http[0] - ) - }) + getChain(biconomySmartAccountConfig.chainId).rpcUrls.default.http[0], + ), + }); this.scanForUpgradedAccountsFromV1 = - biconomySmartAccountConfig.scanForUpgradedAccountsFromV1 ?? false - this.maxIndexForScan = biconomySmartAccountConfig.maxIndexForScan ?? 10 - this.getAccountAddress() + biconomySmartAccountConfig.scanForUpgradedAccountsFromV1 ?? false; + this.maxIndexForScan = biconomySmartAccountConfig.maxIndexForScan ?? 10; + this.getAccountAddress(); } /** @@ -248,45 +258,45 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * */ public static async create( - biconomySmartAccountConfig: BiconomySmartAccountV2Config + biconomySmartAccountConfig: BiconomySmartAccountV2Config, ): Promise { - let chainId = biconomySmartAccountConfig.chainId + let chainId = biconomySmartAccountConfig.chainId; let rpcUrl = biconomySmartAccountConfig.customChain?.rpcUrls?.default?.http?.[0] ?? - biconomySmartAccountConfig.rpcUrl - let resolvedSmartAccountSigner!: SmartAccountSigner + biconomySmartAccountConfig.rpcUrl; + let resolvedSmartAccountSigner!: SmartAccountSigner; // Signer needs to be initialised here before defaultValidationModule is set if (biconomySmartAccountConfig.signer) { const signerResult = await convertSigner( biconomySmartAccountConfig.signer, !!chainId, - rpcUrl - ) + rpcUrl, + ); if (!chainId && !!signerResult.chainId) { - chainId = signerResult.chainId + chainId = signerResult.chainId; } if (!rpcUrl && !!signerResult.rpcUrl) { if (isValidRpcUrl(signerResult.rpcUrl)) { - rpcUrl = signerResult.rpcUrl + rpcUrl = signerResult.rpcUrl; } } - resolvedSmartAccountSigner = signerResult.signer + resolvedSmartAccountSigner = signerResult.signer; } if (!chainId) { // Get it from bundler if (biconomySmartAccountConfig.bundlerUrl) { chainId = extractChainIdFromBundlerUrl( - biconomySmartAccountConfig.bundlerUrl - ) + biconomySmartAccountConfig.bundlerUrl, + ); } else if (biconomySmartAccountConfig.bundler) { const bundlerUrlFromBundler = - biconomySmartAccountConfig.bundler.getBundlerUrl() - chainId = extractChainIdFromBundlerUrl(bundlerUrlFromBundler) + biconomySmartAccountConfig.bundler.getBundlerUrl(); + chainId = extractChainIdFromBundlerUrl(bundlerUrlFromBundler); } } if (!chainId) { - throw new Error("chainId required") + throw new Error("chainId required"); } const bundler: IBundler = @@ -298,27 +308,27 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { customChain: biconomySmartAccountConfig.viemChain ?? biconomySmartAccountConfig.customChain ?? - getChain(chainId) - }) + getChain(chainId), + }); let defaultValidationModule = - biconomySmartAccountConfig.defaultValidationModule + biconomySmartAccountConfig.defaultValidationModule; // Note: If no module is provided, we will use ECDSA_OWNERSHIP as default if (!defaultValidationModule) { const newModule = await createECDSAOwnershipValidationModule({ // biome-ignore lint/style/noNonNullAssertion: - signer: resolvedSmartAccountSigner! - }) - defaultValidationModule = newModule + signer: resolvedSmartAccountSigner!, + }); + defaultValidationModule = newModule; } const activeValidationModule = biconomySmartAccountConfig?.activeValidationModule ?? - defaultValidationModule + defaultValidationModule; if (!resolvedSmartAccountSigner) { - resolvedSmartAccountSigner = await activeValidationModule.getSigner() + resolvedSmartAccountSigner = await activeValidationModule.getSigner(); } if (!resolvedSmartAccountSigner) { - throw new Error("signer required") + throw new Error("signer required"); } const config: BiconomySmartAccountV2ConfigConstructorProps = { ...biconomySmartAccountConfig, @@ -327,8 +337,8 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { chainId, bundler, signer: resolvedSmartAccountSigner, - rpcUrl - } + rpcUrl, + }; // We check if chain ids match (skip this if chainId is passed by in the config) // This check is at the end of the function for cases when the signer is not passed in the config but a validation modules is and we get the signer from the validation module in this case @@ -339,31 +349,31 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { await compareChainIds( biconomySmartAccountConfig.signer || resolvedSmartAccountSigner, config, - false - ) + false, + ); } - return new BiconomySmartAccountV2(config) + return new BiconomySmartAccountV2(config); } // Calls the getCounterFactualAddress override async getAddress(params?: CounterFactualAddressParam): Promise { if (this.accountAddress == null) { // means it needs deployment - this.accountAddress = await this.getCounterFactualAddress(params) + this.accountAddress = await this.getCounterFactualAddress(params); } - return this.accountAddress + return this.accountAddress; } // Calls the getCounterFactualAddress async getAccountAddress( - params?: CounterFactualAddressParam + params?: CounterFactualAddressParam, ): Promise<`0x${string}`> { if (this.accountAddress == null || this.accountAddress === undefined) { // means it needs deployment - this.accountAddress = await this.getCounterFactualAddress(params) + this.accountAddress = await this.getCounterFactualAddress(params); } - return this.accountAddress + return this.accountAddress; } /** @@ -412,32 +422,32 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { */ public async getGasEstimate( transactions: Transaction[], - buildUseropDto?: BuildUserOpOptions + buildUseropDto?: BuildUserOpOptions, ): Promise { const { callGasLimit, preVerificationGas, verificationGasLimit, - maxFeePerGas - } = await this.buildUserOp(transactions, buildUseropDto) + maxFeePerGas, + } = await this.buildUserOp(transactions, buildUseropDto); - const _callGasLimit = BigInt(callGasLimit || 0) - const _preVerificationGas = BigInt(preVerificationGas || 0) - const _verificationGasLimit = BigInt(verificationGasLimit || 0) - const _maxFeePerGas = BigInt(maxFeePerGas || 0) + const _callGasLimit = BigInt(callGasLimit || 0); + const _preVerificationGas = BigInt(preVerificationGas || 0); + const _verificationGasLimit = BigInt(verificationGasLimit || 0); + const _maxFeePerGas = BigInt(maxFeePerGas || 0); if (!buildUseropDto?.paymasterServiceData?.mode) { return ( (_callGasLimit + _preVerificationGas + _verificationGasLimit) * _maxFeePerGas - ) + ); } return ( (_callGasLimit + BigInt(3) * _verificationGasLimit + _preVerificationGas) * _maxFeePerGas - ) + ); } /** @@ -489,30 +499,30 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * */ public async getBalances( - addresses?: Array + addresses?: Array, ): Promise> { - const accountAddress = await this.getAccountAddress() - const result: BalancePayload[] = [] + const accountAddress = await this.getAccountAddress(); + const result: BalancePayload[] = []; if (addresses) { const tokenContracts = addresses.map((address) => getContract({ address, abi: parseAbi(ERC20_ABI), - client: this.provider - }) - ) + client: this.provider, + }), + ); const balancePromises = tokenContracts.map((tokenContract) => - tokenContract.read.balanceOf([accountAddress]) - ) as Promise[] + tokenContract.read.balanceOf([accountAddress]), + ) as Promise[]; const decimalsPromises = tokenContracts.map((tokenContract) => - tokenContract.read.decimals() - ) as Promise[] + tokenContract.read.decimals(), + ) as Promise[]; const [balances, decimalsPerToken] = await Promise.all([ Promise.all(balancePromises), - Promise.all(decimalsPromises) - ]) + Promise.all(decimalsPromises), + ]); balances.forEach((amount, index) => result.push({ @@ -520,22 +530,22 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { decimals: decimalsPerToken[index], address: addresses[index], formattedAmount: formatUnits(amount, decimalsPerToken[index]), - chainId: this.chainId - }) - ) + chainId: this.chainId, + }), + ); } - const balance = await this.provider.getBalance({ address: accountAddress }) + const balance = await this.provider.getBalance({ address: accountAddress }); result.push({ amount: balance, decimals: 18, address: NATIVE_TOKEN_ALIAS, formattedAmount: formatUnits(balance, 18), - chainId: this.chainId - }) + chainId: this.chainId, + }); - return result + return result; } /** @@ -583,36 +593,36 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { public async withdraw( withdrawalRequests?: WithdrawalRequest[] | null, defaultRecipient?: Hex | null, - buildUseropDto?: BuildUserOpOptions + buildUseropDto?: BuildUserOpOptions, ): Promise { const accountAddress = - this.accountAddress ?? (await this.getAccountAddress()) + this.accountAddress ?? (await this.getAccountAddress()); if ( !defaultRecipient && withdrawalRequests?.some(({ recipient }) => !recipient) ) { - throw new Error(ERROR_MESSAGES.NO_RECIPIENT) + throw new Error(ERROR_MESSAGES.NO_RECIPIENT); } // Remove the native token from the withdrawal requests let tokenRequests = withdrawalRequests?.filter( - ({ address }) => !addressEquals(address, NATIVE_TOKEN_ALIAS) - ) ?? [] + ({ address }) => !addressEquals(address, NATIVE_TOKEN_ALIAS), + ) ?? []; // Check if the amount is not present in all withdrawal requests - const shouldFetchMaxBalances = tokenRequests.some(({ amount }) => !amount) + const shouldFetchMaxBalances = tokenRequests.some(({ amount }) => !amount); // Get the balances of the tokens if the amount is not present in the withdrawal requests if (shouldFetchMaxBalances) { const balances = await this.getBalances( - tokenRequests.map(({ address }) => address) - ) + tokenRequests.map(({ address }) => address), + ); tokenRequests = tokenRequests.map(({ amount, address }, i) => ({ address, - amount: amount ?? balances[i].amount - })) + amount: amount ?? balances[i].amount, + })); } // Create the transactions @@ -622,87 +632,87 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { data: encodeFunctionData({ abi: parseAbi(ERC20_ABI), functionName: "transfer", - args: [recipientFromRequest || defaultRecipient, amount] - }) - }) - ) + args: [recipientFromRequest || defaultRecipient, amount], + }), + }), + ); // Check if eth alias is present in the original withdrawal requests const nativeTokenRequest = withdrawalRequests?.find(({ address }) => - addressEquals(address, NATIVE_TOKEN_ALIAS) - ) - const hasNoRequests = !withdrawalRequests?.length + addressEquals(address, NATIVE_TOKEN_ALIAS), + ); + const hasNoRequests = !withdrawalRequests?.length; if (!!nativeTokenRequest || hasNoRequests) { // Check that an amount is present in the withdrawal request, if no paymaster service data is present, as max amounts cannot be calculated without a paymaster. if ( !nativeTokenRequest?.amount && !buildUseropDto?.paymasterServiceData?.mode ) { - throw new Error(ERROR_MESSAGES.NATIVE_TOKEN_WITHDRAWAL_WITHOUT_AMOUNT) + throw new Error(ERROR_MESSAGES.NATIVE_TOKEN_WITHDRAWAL_WITHOUT_AMOUNT); } // get eth balance if not present in withdrawal requests const nativeTokenAmountToWithdraw = nativeTokenRequest?.amount ?? - (await this.provider.getBalance({ address: accountAddress })) + (await this.provider.getBalance({ address: accountAddress })); txs.push({ to: (nativeTokenRequest?.recipient ?? defaultRecipient) as Hex, - value: nativeTokenAmountToWithdraw - }) + value: nativeTokenAmountToWithdraw, + }); } - return this.sendTransaction(txs, buildUseropDto) + return this.sendTransaction(txs, buildUseropDto); } /** * Return the account's address. This value is valid even before deploying the contract. */ async getCounterFactualAddress( - params?: CounterFactualAddressParam + params?: CounterFactualAddressParam, ): Promise { const validationModule = - params?.validationModule ?? this.defaultValidationModule - const index = params?.index ?? this.index + params?.validationModule ?? this.defaultValidationModule; + const index = params?.index ?? this.index; - const maxIndexForScan = params?.maxIndexForScan ?? this.maxIndexForScan + const maxIndexForScan = params?.maxIndexForScan ?? this.maxIndexForScan; // Review: default behavior const scanForUpgradedAccountsFromV1 = params?.scanForUpgradedAccountsFromV1 ?? - this.scanForUpgradedAccountsFromV1 + this.scanForUpgradedAccountsFromV1; // if it's intended to detect V1 upgraded accounts if (scanForUpgradedAccountsFromV1) { - const eoaSigner = await validationModule.getSigner() - const eoaAddress = (await eoaSigner.getAddress()) as Hex - const moduleAddress = validationModule.getAddress() as Hex - const moduleSetupData = (await validationModule.getInitData()) as Hex + const eoaSigner = await validationModule.getSigner(); + const eoaAddress = (await eoaSigner.getAddress()) as Hex; + const moduleAddress = validationModule.getAddress() as Hex; + const moduleSetupData = (await validationModule.getInitData()) as Hex; const queryParams = { eoaAddress, index, moduleAddress, moduleSetupData, - maxIndexForScan - } - const accountAddress = await this.getV1AccountsUpgradedToV2(queryParams) + maxIndexForScan, + }; + const accountAddress = await this.getV1AccountsUpgradedToV2(queryParams); if (accountAddress !== ADDRESS_ZERO) { - return accountAddress + return accountAddress; } } const counterFactualAddressV2 = await this.getCounterFactualAddressV2({ validationModule, - index - }) - return counterFactualAddressV2 + index, + }); + return counterFactualAddressV2; } private async getCounterFactualAddressV2( - params?: CounterFactualAddressParam + params?: CounterFactualAddressParam, ): Promise { const validationModule = - params?.validationModule ?? this.defaultValidationModule - const index = params?.index ?? this.index + params?.validationModule ?? this.defaultValidationModule; + const index = params?.index ?? this.index; try { const initCalldata = encodeFunctionData({ @@ -711,33 +721,33 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { args: [ this.defaultFallbackHandlerAddress, validationModule.getAddress() as Hex, - (await validationModule.getInitData()) as Hex - ] - }) + (await validationModule.getInitData()) as Hex, + ], + }); const proxyCreationCodeHash = keccak256( encodePacked( ["bytes", "uint256"], - [PROXY_CREATION_CODE, BigInt(this.implementationAddress)] - ) - ) + [PROXY_CREATION_CODE, BigInt(this.implementationAddress)], + ), + ); const salt = keccak256( encodePacked( ["bytes32", "uint256"], - [keccak256(initCalldata), BigInt(index)] - ) - ) + [keccak256(initCalldata), BigInt(index)], + ), + ); const counterFactualAddress = getCreate2Address({ from: this.factoryAddress, salt: salt, - bytecodeHash: proxyCreationCodeHash - }) + bytecodeHash: proxyCreationCodeHash, + }); - return counterFactualAddress + return counterFactualAddress; } catch (e) { - throw new Error(`Failed to get counterfactual address, ${e}`) + throw new Error(`Failed to get counterfactual address, ${e}`); } } @@ -748,54 +758,54 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { this.accountContract = getContract({ address: await this.getAddress(), abi: BiconomyAccountAbi, - client: this.provider as PublicClient - }) + client: this.provider as PublicClient, + }); } - return this.accountContract + return this.accountContract; } isActiveValidationModuleDefined(): boolean { if (!this.activeValidationModule) - throw new Error("Must provide an instance of active validation module.") - return true + throw new Error("Must provide an instance of active validation module."); + return true; } isDefaultValidationModuleDefined(): boolean { if (!this.defaultValidationModule) - throw new Error("Must provide an instance of default validation module.") - return true + throw new Error("Must provide an instance of default validation module."); + return true; } setActiveValidationModule( - validationModule: BaseValidationModule + validationModule: BaseValidationModule, ): BiconomySmartAccountV2 { if (validationModule instanceof BaseValidationModule) { - this.activeValidationModule = validationModule + this.activeValidationModule = validationModule; } - return this + return this; } setDefaultValidationModule( - validationModule: BaseValidationModule + validationModule: BaseValidationModule, ): BiconomySmartAccountV2 { if (validationModule instanceof BaseValidationModule) { - this.defaultValidationModule = validationModule + this.defaultValidationModule = validationModule; } - return this + return this; } async getV1AccountsUpgradedToV2( - params: QueryParamsForAddressResolver + params: QueryParamsForAddressResolver, ): Promise { - const maxIndexForScan = params.maxIndexForScan ?? this.maxIndexForScan + const maxIndexForScan = params.maxIndexForScan ?? this.maxIndexForScan; const addressResolver = getContract({ address: ADDRESS_RESOLVER_ADDRESS, abi: AccountResolverAbi, client: { - public: this.provider as PublicClient - } - }) + public: this.provider as PublicClient, + }, + }); // Note: depending on moduleAddress and moduleSetupData passed call this. otherwise could call resolveAddresses() if (params.moduleAddress && params.moduleSetupData) { @@ -803,27 +813,27 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { params.eoaAddress, maxIndexForScan, params.moduleAddress, - params.moduleSetupData - ]) + params.moduleSetupData, + ]); const desiredV1Account = result.find( (smartAccountInfo: { - factoryVersion: string - currentVersion: string - deploymentIndex: { toString: () => string } + factoryVersion: string; + currentVersion: string; + deploymentIndex: { toString: () => string }; }) => smartAccountInfo.factoryVersion === "v1" && smartAccountInfo.currentVersion === "2.0.0" && - Number(smartAccountInfo.deploymentIndex.toString()) === params.index - ) + Number(smartAccountInfo.deploymentIndex.toString()) === params.index, + ); if (desiredV1Account) { - const smartAccountAddress = desiredV1Account.accountAddress - return smartAccountAddress + const smartAccountAddress = desiredV1Account.accountAddress; + return smartAccountAddress; } - return ADDRESS_ZERO + return ADDRESS_ZERO; } - return ADDRESS_ZERO + return ADDRESS_ZERO; } /** @@ -831,14 +841,14 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * This value holds the "factory" address, followed by this account's information */ async getAccountInitCode(): Promise { - this.isDefaultValidationModuleDefined() + this.isDefaultValidationModuleDefined(); - if (await this.isAccountDeployed()) return "0x" + if (await this.isAccountDeployed()) return "0x"; return concatHex([ this.factoryAddress as Hex, - (await this.getFactoryData()) ?? "0x" - ]) + (await this.getFactoryData()) ?? "0x", + ]); } /** @@ -853,8 +863,8 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { return encodeFunctionData({ abi: BiconomyAccountAbi, functionName: "execute_ncC", - args: [to, value, data] - }) + args: [to, value, data], + }); } /** @@ -867,68 +877,76 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { async encodeExecuteBatch( to: Array, value: Array, - data: Array + data: Array, ): Promise { return encodeFunctionData({ abi: BiconomyAccountAbi, functionName: "executeBatch_y6U", - args: [to, value, data] - }) + args: [to, value, data], + }); } override async encodeBatchExecute( - txs: BatchUserOperationCallData + txs: BatchUserOperationCallData, ): Promise { const [targets, datas, value] = txs.reduce( (accum, curr) => { - accum[0].push(curr.target) - accum[1].push(curr.data) - accum[2].push(curr.value || BigInt(0)) + accum[0].push(curr.target); + accum[1].push(curr.data); + accum[2].push(curr.value || BigInt(0)); - return accum + return accum; }, - [[], [], []] as [Hex[], Hex[], bigint[]] - ) + [[], [], []] as [Hex[], Hex[], bigint[]], + ); - return this.encodeExecuteBatch(targets, value, datas) + return this.encodeExecuteBatch(targets, value, datas); } // dummy signature depends on the validation module supplied. - async getDummySignatures(_params?: ModuleInfo): Promise { - const params = { ...(this.sessionData ? this.sessionData : {}), ..._params } - this.isActiveValidationModuleDefined() - return (await this.activeValidationModule.getDummySignature(params)) as Hex + async getDummySignatures(params?: ModuleInfo): Promise { + const defaultedParams = { + ...(this.sessionData ? this.sessionData : {}), + ...params, + }; + + this.isActiveValidationModuleDefined(); + return (await this.activeValidationModule.getDummySignature( + defaultedParams, + )) as Hex; } // TODO: review this getDummySignature(): Hex { - throw new Error("Method not implemented! Call getDummySignatures instead.") + throw new Error("Method not implemented! Call getDummySignatures instead."); } // Might use provided paymaster instance to get dummy data (from pm service) getDummyPaymasterData(): string { - return "0x" + return "0x"; } validateUserOp( userOp: Partial, - requiredFields: UserOperationKey[] + requiredFields: UserOperationKey[], ): boolean { for (const field of requiredFields) { if (isNullOrUndefined(userOp[field])) { - throw new Error(`${String(field)} is missing in the UserOp`) + throw new Error(`${String(field)} is missing in the UserOp`); } } - return true + return true; } async signUserOp( userOp: Partial, - _params?: SendUserOpParams + params?: SendUserOpParams, ): Promise { - const params = { ...(this.sessionData ? this.sessionData : {}), ..._params } - - this.isActiveValidationModuleDefined() + const defaultedParams = { + ...(this.sessionData ? this.sessionData : {}), + ...params, + }; + this.isActiveValidationModuleDefined(); const requiredFields: UserOperationKey[] = [ "sender", "nonce", @@ -939,124 +957,128 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { "preVerificationGas", "maxFeePerGas", "maxPriorityFeePerGas", - "paymasterAndData" - ] - this.validateUserOp(userOp, requiredFields) - const userOpHash = await this.getUserOpHash(userOp) + "paymasterAndData", + ]; + this.validateUserOp(userOp, requiredFields); + + const userOpHash = await this.getUserOpHash(userOp); const moduleSig = (await this.activeValidationModule.signUserOpHash( userOpHash, - params - )) as Hex + defaultedParams, + )) as Hex; const signatureWithModuleAddress = this.getSignatureWithModuleAddress( moduleSig, - this.activeValidationModule.getAddress() as Hex - ) + this.activeValidationModule.getAddress() as Hex, + ); + + userOp.signature = signatureWithModuleAddress; - userOp.signature = signatureWithModuleAddress - return userOp as UserOperationStruct + return userOp as UserOperationStruct; } getSignatureWithModuleAddress( moduleSignature: Hex, - moduleAddress?: Hex + moduleAddress?: Hex, ): Hex { const moduleAddressToUse = - moduleAddress ?? (this.activeValidationModule.getAddress() as Hex) - return encodeAbiParameters(parseAbiParameters("bytes, address"), [ + moduleAddress ?? (this.activeValidationModule.getAddress() as Hex); + const result = encodeAbiParameters(parseAbiParameters("bytes, address"), [ moduleSignature, - moduleAddressToUse - ]) + moduleAddressToUse, + ]); + + return result; } public async getPaymasterUserOp( userOp: Partial, - paymasterServiceData: PaymasterUserOperationDto + paymasterServiceData: PaymasterUserOperationDto, ): Promise> { if (paymasterServiceData.mode === PaymasterMode.SPONSORED) { - return this.getPaymasterAndData(userOp, paymasterServiceData) + return this.getPaymasterAndData(userOp, paymasterServiceData); } if (paymasterServiceData.mode === PaymasterMode.ERC20) { if (paymasterServiceData?.feeQuote) { - const { feeQuote, spender, maxApproval = false } = paymasterServiceData - Logger.log("there is a feeQuote: ", JSON.stringify(feeQuote, null, 2)) - if (!spender) throw new Error(ERROR_MESSAGES.SPENDER_REQUIRED) - if (!feeQuote) throw new Error(ERROR_MESSAGES.FAILED_FEE_QUOTE_FETCH) + const { feeQuote, spender, maxApproval = false } = paymasterServiceData; + Logger.log("there is a feeQuote: ", JSON.stringify(feeQuote, null, 2)); + if (!spender) throw new Error(ERROR_MESSAGES.SPENDER_REQUIRED); + if (!feeQuote) throw new Error(ERROR_MESSAGES.FAILED_FEE_QUOTE_FETCH); if ( paymasterServiceData.skipPatchCallData && paymasterServiceData.skipPatchCallData === true ) { return this.getPaymasterAndData(userOp, { ...paymasterServiceData, - feeTokenAddress: feeQuote.tokenAddress - }) + feeTokenAddress: feeQuote.tokenAddress, + }); } const partialUserOp = await this.buildTokenPaymasterUserOp(userOp, { ...paymasterServiceData, spender, maxApproval, - feeQuote - }) + feeQuote, + }); return this.getPaymasterAndData(partialUserOp, { ...paymasterServiceData, feeTokenAddress: feeQuote.tokenAddress, - calculateGasLimits: paymasterServiceData.calculateGasLimits ?? true // Always recommended and especially when using token paymaster - }) + calculateGasLimits: paymasterServiceData.calculateGasLimits ?? true, // Always recommended and especially when using token paymaster + }); } if (paymasterServiceData?.preferredToken) { - const { preferredToken } = paymasterServiceData - Logger.log("there is a preferred token: ", preferredToken) + const { preferredToken } = paymasterServiceData; + Logger.log("there is a preferred token: ", preferredToken); const feeQuotesResponse = await this.getPaymasterFeeQuotesOrData( userOp, - paymasterServiceData - ) - const spender = feeQuotesResponse.tokenPaymasterAddress - const feeQuote = feeQuotesResponse.feeQuotes?.[0] - if (!spender) throw new Error(ERROR_MESSAGES.SPENDER_REQUIRED) - if (!feeQuote) throw new Error(ERROR_MESSAGES.FAILED_FEE_QUOTE_FETCH) + paymasterServiceData, + ); + const spender = feeQuotesResponse.tokenPaymasterAddress; + const feeQuote = feeQuotesResponse.feeQuotes?.[0]; + if (!spender) throw new Error(ERROR_MESSAGES.SPENDER_REQUIRED); + if (!feeQuote) throw new Error(ERROR_MESSAGES.FAILED_FEE_QUOTE_FETCH); return this.getPaymasterUserOp(userOp, { ...paymasterServiceData, feeQuote, - spender - }) // Recursively call getPaymasterUserOp with the feeQuote + spender, + }); // Recursively call getPaymasterUserOp with the feeQuote } Logger.log( - "ERC20 mode without feeQuote or preferredToken provided. Passing through unchanged." - ) - return userOp + "ERC20 mode without feeQuote or preferredToken provided. Passing through unchanged.", + ); + return userOp; } - throw new Error("Invalid paymaster mode") + throw new Error("Invalid paymaster mode"); } private async getPaymasterAndData( userOp: Partial, - paymasterServiceData: PaymasterUserOperationDto + paymasterServiceData: PaymasterUserOperationDto, ): Promise> { const paymaster = this - .paymaster as IHybridPaymaster + .paymaster as IHybridPaymaster; const paymasterData = await paymaster.getPaymasterAndData( userOp, - paymasterServiceData - ) - return { ...userOp, ...paymasterData } + paymasterServiceData, + ); + return { ...userOp, ...paymasterData }; } private async getPaymasterFeeQuotesOrData( userOp: Partial, - feeQuotesOrData: FeeQuotesOrDataDto + feeQuotesOrData: FeeQuotesOrDataDto, ): Promise { const paymaster = this - .paymaster as IHybridPaymaster + .paymaster as IHybridPaymaster; const tokenList = feeQuotesOrData?.preferredToken ? [feeQuotesOrData?.preferredToken] : feeQuotesOrData?.tokenList?.length ? feeQuotesOrData?.tokenList - : [] + : []; return paymaster.getPaymasterFeeQuotesOrData(userOp, { ...feeQuotesOrData, - tokenList - }) + tokenList, + }); } /** @@ -1108,18 +1130,18 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { */ public async getTokenFees( manyOrOneTransactions: Transaction | Transaction[], - buildUseropDto: BuildUserOpOptions + buildUseropDto: BuildUserOpOptions, ): Promise { const txs = Array.isArray(manyOrOneTransactions) ? manyOrOneTransactions - : [manyOrOneTransactions] - const userOp = await this.buildUserOp(txs, buildUseropDto) + : [manyOrOneTransactions]; + const userOp = await this.buildUserOp(txs, buildUseropDto); if (!buildUseropDto.paymasterServiceData) - throw new Error("paymasterServiceData was not provided") + throw new Error("paymasterServiceData was not provided"); return this.getPaymasterFeeQuotesOrData( userOp, - buildUseropDto.paymasterServiceData - ) + buildUseropDto.paymasterServiceData, + ); } /** @@ -1158,28 +1180,28 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { { data: "0x", value: BigInt(0), - to: await this.getAccountAddress() + to: await this.getAccountAddress(), }, { - paymasterServiceData: { mode: PaymasterMode.ERC20 } - } - ) + paymasterServiceData: { mode: PaymasterMode.ERC20 }, + }, + ); return await Promise.all( (feeQuotesResponse?.feeQuotes ?? []).map(async (quote) => { const [tokenBalance] = await this.getBalances([ - quote.tokenAddress as Hex - ]) + quote.tokenAddress as Hex, + ]); return { symbol: quote.symbol, tokenAddress: quote.tokenAddress, decimal: quote.decimal, logoUrl: quote.logoUrl, premiumPercentage: quote.premiumPercentage, - balance: tokenBalance - } - }) - ) + balance: tokenBalance, + }; + }), + ); } /** @@ -1228,18 +1250,15 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { */ async sendUserOp( userOp: Partial, - params?: SendUserOpParams + params?: SendUserOpParams, ): Promise { // biome-ignore lint/performance/noDelete: - delete userOp.signature - const userOperation = await this.signUserOp(userOp, params) + delete userOp.signature; + const userOperation = await this.signUserOp(userOp, params); - const bundlerResponse = await this.sendSignedUserOp( - userOperation, - params?.simulationType - ) + const bundlerResponse = await this.sendSignedUserOp(userOperation); - return bundlerResponse + return bundlerResponse; } /** @@ -1251,7 +1270,7 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { */ async sendSignedUserOp( userOp: UserOperationStruct, - simulationType?: SimulationType + simulationType?: SimulationType, ): Promise { const requiredFields: UserOperationKey[] = [ "sender", @@ -1264,44 +1283,44 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { "maxFeePerGas", "maxPriorityFeePerGas", "paymasterAndData", - "signature" - ] - this.validateUserOp(userOp, requiredFields) - if (!this.bundler) throw new Error("Bundler is not provided") + "signature", + ]; + this.validateUserOp(userOp, requiredFields); + if (!this.bundler) throw new Error("Bundler is not provided"); Logger.warn( "userOp being sent to the bundler", - JSON.stringify(userOp, null, 2) - ) + JSON.stringify(userOp, null, 2), + ); const bundlerResponse = await this.bundler.sendUserOp( userOp, - simulationType - ) - return bundlerResponse + simulationType, + ); + return bundlerResponse; } async getUserOpHash(userOp: Partial): Promise { - const userOpHash = keccak256(packUserOp(userOp, true) as Hex) + const userOpHash = keccak256(packUserOp(userOp, true) as Hex); const enc = encodeAbiParameters( parseAbiParameters("bytes32, address, uint256"), - [userOpHash, this.entryPoint.address, BigInt(this.chainId)] - ) - return keccak256(enc) + [userOpHash, this.entryPoint.address, BigInt(this.chainId)], + ); + return keccak256(enc); } async estimateUserOpGas( userOp: Partial, - stateOverrideSet?: StateOverrideSet + stateOverrideSet?: StateOverrideSet, ): Promise> { - if (!this.bundler) throw new Error("Bundler is not provided") + if (!this.bundler) throw new Error("Bundler is not provided"); const requiredFields: UserOperationKey[] = [ "sender", "nonce", "initCode", - "callData" - ] - this.validateUserOp(userOp, requiredFields) + "callData", + ]; + this.validateUserOp(userOp, requiredFields); - const finalUserOp = userOp + const finalUserOp = userOp; // Making call to bundler to get gas estimations for userOp const { @@ -1309,86 +1328,84 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { verificationGasLimit, preVerificationGas, maxFeePerGas, - maxPriorityFeePerGas - } = await this.bundler.estimateUserOpGas(userOp, stateOverrideSet) + maxPriorityFeePerGas, + } = await this.bundler.estimateUserOpGas(userOp, stateOverrideSet); // if neither user sent gas fee nor the bundler, estimate gas from provider if ( !userOp.maxFeePerGas && !userOp.maxPriorityFeePerGas && (!maxFeePerGas || !maxPriorityFeePerGas) ) { - const feeData = await this.provider.estimateFeesPerGas() + const feeData = await this.provider.estimateFeesPerGas(); if (feeData.maxFeePerGas?.toString()) { finalUserOp.maxFeePerGas = `0x${feeData.maxFeePerGas.toString( - 16 - )}` as Hex + 16, + )}` as Hex; } else if (feeData.gasPrice?.toString()) { - finalUserOp.maxFeePerGas = `0x${feeData.gasPrice.toString(16)}` as Hex + finalUserOp.maxFeePerGas = `0x${feeData.gasPrice.toString(16)}` as Hex; } else { - finalUserOp.maxFeePerGas = `0x${( - await this.provider.getGasPrice() - ).toString(16)}` as Hex + finalUserOp.maxFeePerGas = + `0x${(await this.provider.getGasPrice()).toString(16)}` as Hex; } if (feeData.maxPriorityFeePerGas?.toString()) { finalUserOp.maxPriorityFeePerGas = - `0x${feeData.maxPriorityFeePerGas?.toString()}` as Hex + `0x${feeData.maxPriorityFeePerGas?.toString()}` as Hex; } else if (feeData.gasPrice?.toString()) { finalUserOp.maxPriorityFeePerGas = toHex( - Number(feeData.gasPrice?.toString()) - ) + Number(feeData.gasPrice?.toString()), + ); } else { - finalUserOp.maxPriorityFeePerGas = `0x${( - await this.provider.getGasPrice() - ).toString(16)}` as Hex + finalUserOp.maxPriorityFeePerGas = + `0x${(await this.provider.getGasPrice()).toString(16)}` as Hex; } } else { finalUserOp.maxFeePerGas = - toHex(Number(maxFeePerGas)) ?? userOp.maxFeePerGas + toHex(Number(maxFeePerGas)) ?? userOp.maxFeePerGas; finalUserOp.maxPriorityFeePerGas = - toHex(Number(maxPriorityFeePerGas)) ?? userOp.maxPriorityFeePerGas + toHex(Number(maxPriorityFeePerGas)) ?? userOp.maxPriorityFeePerGas; } finalUserOp.verificationGasLimit = - toHex(Number(verificationGasLimit)) ?? userOp.verificationGasLimit + toHex(Number(verificationGasLimit)) ?? userOp.verificationGasLimit; finalUserOp.callGasLimit = - toHex(Number(callGasLimit)) ?? userOp.callGasLimit + toHex(Number(callGasLimit)) ?? userOp.callGasLimit; finalUserOp.preVerificationGas = - toHex(Number(preVerificationGas)) ?? userOp.preVerificationGas + toHex(Number(preVerificationGas)) ?? userOp.preVerificationGas; if (!finalUserOp.paymasterAndData) { - finalUserOp.paymasterAndData = "0x" + finalUserOp.paymasterAndData = "0x"; } - return finalUserOp + return finalUserOp; } override async getNonce(nonceKey?: number): Promise { - const nonceSpace = nonceKey ?? 0 + const nonceSpace = nonceKey ?? 0; try { - const address = await this.getAddress() - return await this.entryPoint.read.getNonce([address, BigInt(nonceSpace)]) + const address = await this.getAddress(); + return await this.entryPoint.read.getNonce([address, BigInt(nonceSpace)]); } catch (e) { - return BigInt(0) + return BigInt(0); } } private async getBuildUserOpNonce( - nonceOptions: NonceOptions | undefined + nonceOptions: NonceOptions | undefined, ): Promise { - let nonce = BigInt(0) + let nonce = BigInt(0); try { if (nonceOptions?.nonceOverride) { - nonce = BigInt(nonceOptions?.nonceOverride) + nonce = BigInt(nonceOptions?.nonceOverride); } else { - const _nonceSpace = nonceOptions?.nonceKey ?? 0 - nonce = await this.getNonce(_nonceSpace) + const _nonceSpace = nonceOptions?.nonceKey ?? 0; + nonce = await this.getNonce(_nonceSpace); } } catch (error) { // Not throwing this error as nonce would be 0 if this.getNonce() throw exception, which is expected flow for undeployed account Logger.warn( - "Error while getting nonce for the account. This is expected for undeployed accounts set nonce to 0" - ) + "Error while getting nonce for the account. This is expected for undeployed accounts set nonce to 0", + ); } - return nonce + return nonce; } /** @@ -1434,22 +1451,22 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { async transferOwnership( newOwner: Address, moduleAddress: TransferOwnershipCompatibleModule, - buildUseropDto?: BuildUserOpOptions + buildUseropDto?: BuildUserOpOptions, ): Promise { const encodedCall = encodeFunctionData({ abi: parseAbi(["function transferOwnership(address newOwner) public"]), functionName: "transferOwnership", - args: [newOwner] - }) + args: [newOwner], + }); const transaction = { to: moduleAddress, - data: encodedCall - } + data: encodedCall, + }; const userOpResponse: UserOpResponse = await this.sendTransaction( transaction, - buildUseropDto - ) - return userOpResponse + buildUseropDto, + ); + return userOpResponse; } /** @@ -1459,6 +1476,7 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * * @param manyOrOneTransactions Array of {@link Transaction} to be batched and sent. Can also be a single {@link Transaction}. * @param buildUseropDto {@link BuildUserOpOptions}. + * @param sessionData * @returns Promise<{@link UserOpResponse}> that you can use to track the user operation. * * @example @@ -1490,6 +1508,7 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * * @remarks * This example shows how to increase the estimated gas values for a transaction using `gasOffset` parameter. + * * @example * import { createClient } from "viem" * import { createSmartAccountClient } from "@biconomy/account" @@ -1525,18 +1544,90 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { */ async sendTransaction( manyOrOneTransactions: Transaction | Transaction[], - buildUseropDto?: BuildUserOpOptions + buildUseropDto?: BuildUserOpOptions, + sessionData?: Prettify>, ): Promise { + let defaultedBuildUseropDto = { ...buildUseropDto } ?? {}; + + if (this.sessionType && sessionData) { + const getSessionParameters = await this.getSessionParams( + ...(sessionData ?? []), + ); + defaultedBuildUseropDto = { + ...defaultedBuildUseropDto, + ...getSessionParameters, + }; + } + const userOp = await this.buildUserOp( Array.isArray(manyOrOneTransactions) ? manyOrOneTransactions : [manyOrOneTransactions], - buildUseropDto - ) - return this.sendUserOp(userOp, { - simulationType: buildUseropDto?.simulationType, - ...buildUseropDto?.params - }) + defaultedBuildUseropDto, + ); + + if (defaultedBuildUseropDto?.params?.danModuleInfo) { + defaultedBuildUseropDto.params.danModuleInfo.userOperation = { + ...userOp, + }; + } + + return this.sendUserOp(userOp, { ...defaultedBuildUseropDto?.params }); + } + + public async getSessionParams( + correspondingIndexes?: number[] | number | undefined | null, + conditionalSession?: SessionSearchParam, + chain?: Chain, + txs?: Transaction | Transaction[], + ): Promise<{ params: ModuleInfo }> { + const defaultedTransactions: Transaction[] | null = txs + ? Array.isArray(txs) + ? [...txs] + : [txs] + : []; + + const defaultedConditionalSession: SessionSearchParam = + conditionalSession ?? (await this.getAccountAddress()); + + const defaultedCorrespondingIndexes: number[] | null = correspondingIndexes + ? Array.isArray(correspondingIndexes) + ? [...correspondingIndexes] + : [correspondingIndexes] + : null; + + const correspondingIndex: number | null = defaultedCorrespondingIndexes + ? defaultedCorrespondingIndexes[0] + : null; + + const defaultedChain: Chain = + chain ?? getChain(await this.provider.getChainId()); + + if (!defaultedChain) throw new Error("Chain is not provided"); + + if (this.sessionType === "DAN") { + return getDanSessionTxParams( + defaultedConditionalSession, + defaultedChain, + correspondingIndex, + ); + } + if (this.sessionType === "BATCHED") { + return getBatchSessionTxParams( + defaultedTransactions, + defaultedCorrespondingIndexes, + defaultedConditionalSession, + defaultedChain, + ); + } + if (this.sessionType === "SINGLE") { + return getSingleSessionTxParams( + defaultedConditionalSession, + defaultedChain, + correspondingIndex, + ); + } + throw new Error("Session type is not provided"); } /** @@ -1579,37 +1670,37 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { */ async buildUserOp( transactions: Transaction[], - buildUseropDto?: BuildUserOpOptions + buildUseropDto?: BuildUserOpOptions, ): Promise> { - const to = transactions.map((element: Transaction) => element.to as Hex) + const to = transactions.map((element: Transaction) => element.to as Hex); const data = transactions.map( - (element: Transaction) => (element.data as Hex) ?? "0x" - ) + (element: Transaction) => (element.data as Hex) ?? "0x", + ); const value = transactions.map( - (element: Transaction) => (element.value as bigint) ?? BigInt(0) - ) + (element: Transaction) => (element.value as bigint) ?? BigInt(0), + ); - const initCodeFetchPromise = this.getInitCode() + const initCodeFetchPromise = this.getInitCode(); const dummySignatureFetchPromise = this.getDummySignatures( - buildUseropDto?.params - ) + buildUseropDto?.params, + ); const [nonceFromFetch, initCode, signature] = await Promise.all([ this.getBuildUserOpNonce(buildUseropDto?.nonceOptions), initCodeFetchPromise, - dummySignatureFetchPromise - ]) + dummySignatureFetchPromise, + ]); if (transactions.length === 0) { - throw new Error("Transactions array cannot be empty") + throw new Error("Transactions array cannot be empty"); } - let callData: Hex = "0x" + let callData: Hex = "0x"; if (!buildUseropDto?.useEmptyDeployCallData) { if (transactions.length > 1 || buildUseropDto?.forceEncodeForBatch) { - callData = await this.encodeExecuteBatch(to, value, data) + callData = await this.encodeExecuteBatch(to, value, data); } else { // transactions.length must be 1 - callData = await this.encodeExecute(to[0], value[0], data[0]) + callData = await this.encodeExecute(to[0], value[0], data[0]); } } @@ -1617,101 +1708,101 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { sender: (await this.getAccountAddress()) as Hex, nonce: toHex(nonceFromFetch), initCode, - callData - } + callData, + }; // for this Smart Account current validation module dummy signature will be used to estimate gas - userOp.signature = signature - userOp.paymasterAndData = buildUseropDto?.dummyPndOverride ?? "0x" + userOp.signature = signature; + userOp.paymasterAndData = buildUseropDto?.dummyPndOverride ?? "0x"; if ( buildUseropDto?.paymasterServiceData && buildUseropDto?.paymasterServiceData.mode === PaymasterMode.SPONSORED && this.paymaster instanceof BiconomyPaymaster ) { - const gasFeeValues = await this.bundler?.getGasFeeValues() + const gasFeeValues = await this.bundler?.getGasFeeValues(); // populate gasfee values and make a call to paymaster - userOp.maxFeePerGas = gasFeeValues?.maxFeePerGas as Hex - userOp.maxPriorityFeePerGas = gasFeeValues?.maxPriorityFeePerGas as Hex + userOp.maxFeePerGas = gasFeeValues?.maxFeePerGas as Hex; + userOp.maxPriorityFeePerGas = gasFeeValues?.maxPriorityFeePerGas as Hex; if (buildUseropDto.gasOffset) { - userOp = await this.estimateUserOpGas(userOp) + userOp = await this.estimateUserOpGas(userOp); const { verificationGasLimitOffsetPct, preVerificationGasOffsetPct, callGasLimitOffsetPct, maxFeePerGasOffsetPct, - maxPriorityFeePerGasOffsetPct - } = buildUseropDto.gasOffset + maxPriorityFeePerGasOffsetPct, + } = buildUseropDto.gasOffset; userOp.verificationGasLimit = toHex( Number.parseInt( ( Number(userOp.verificationGasLimit ?? 0) * convertToFactor(verificationGasLimitOffsetPct) - ).toString() - ) - ) + ).toString(), + ), + ); userOp.preVerificationGas = toHex( Number.parseInt( ( Number(userOp.preVerificationGas ?? 0) * convertToFactor(preVerificationGasOffsetPct) - ).toString() - ) - ) + ).toString(), + ), + ); userOp.callGasLimit = toHex( Number.parseInt( ( Number(userOp.callGasLimit ?? 0) * convertToFactor(callGasLimitOffsetPct) - ).toString() - ) - ) + ).toString(), + ), + ); userOp.maxFeePerGas = toHex( Number.parseInt( ( Number(userOp.maxFeePerGas ?? 0) * convertToFactor(maxFeePerGasOffsetPct) - ).toString() - ) - ) + ).toString(), + ), + ); userOp.maxPriorityFeePerGas = toHex( Number.parseInt( ( Number(userOp.maxPriorityFeePerGas ?? 0) * convertToFactor(maxPriorityFeePerGasOffsetPct) - ).toString() - ) - ) + ).toString(), + ), + ); userOp = await this.getPaymasterUserOp(userOp, { ...buildUseropDto.paymasterServiceData, - calculateGasLimits: false - }) - return userOp + calculateGasLimits: false, + }); + return userOp; } if (buildUseropDto.paymasterServiceData.calculateGasLimits === false) { - userOp = await this.estimateUserOpGas(userOp) + userOp = await this.estimateUserOpGas(userOp); } userOp = await this.getPaymasterUserOp( userOp, - buildUseropDto.paymasterServiceData - ) + buildUseropDto.paymasterServiceData, + ); - return userOp + return userOp; } - userOp = await this.estimateUserOpGas(userOp) + userOp = await this.estimateUserOpGas(userOp); if (buildUseropDto?.gasOffset) { if (buildUseropDto?.paymasterServiceData) { userOp = await this.getPaymasterUserOp(userOp, { ...buildUseropDto.paymasterServiceData, - calculateGasLimits: false - }) + calculateGasLimits: false, + }); } const { @@ -1719,84 +1810,84 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { preVerificationGasOffsetPct, callGasLimitOffsetPct, maxFeePerGasOffsetPct, - maxPriorityFeePerGasOffsetPct - } = buildUseropDto.gasOffset + maxPriorityFeePerGasOffsetPct, + } = buildUseropDto.gasOffset; userOp.verificationGasLimit = toHex( Number.parseInt( ( Number(userOp.verificationGasLimit ?? 0) * convertToFactor(verificationGasLimitOffsetPct) - ).toString() - ) - ) + ).toString(), + ), + ); userOp.preVerificationGas = toHex( Number.parseInt( ( Number(userOp.preVerificationGas ?? 0) * convertToFactor(preVerificationGasOffsetPct) - ).toString() - ) - ) + ).toString(), + ), + ); userOp.callGasLimit = toHex( Number.parseInt( ( Number(userOp.callGasLimit ?? 0) * convertToFactor(callGasLimitOffsetPct) - ).toString() - ) - ) + ).toString(), + ), + ); userOp.maxFeePerGas = toHex( Number.parseInt( ( Number(userOp.maxFeePerGas ?? 0) * convertToFactor(maxFeePerGasOffsetPct) - ).toString() - ) - ) + ).toString(), + ), + ); userOp.maxPriorityFeePerGas = toHex( Number.parseInt( ( Number(userOp.maxPriorityFeePerGas ?? 0) * convertToFactor(maxPriorityFeePerGasOffsetPct) - ).toString() - ) - ) + ).toString(), + ), + ); - return userOp + return userOp; } if (buildUseropDto?.paymasterServiceData) { userOp = await this.getPaymasterUserOp( userOp, - buildUseropDto.paymasterServiceData - ) + buildUseropDto.paymasterServiceData, + ); } - return userOp + return userOp; } private validateUserOpAndPaymasterRequest( userOp: Partial, - tokenPaymasterRequest: BiconomyTokenPaymasterRequest + tokenPaymasterRequest: BiconomyTokenPaymasterRequest, ): void { if (isNullOrUndefined(userOp.callData)) { - throw new Error("UserOp callData cannot be undefined") + throw new Error("UserOp callData cannot be undefined"); } - const feeTokenAddress = tokenPaymasterRequest?.feeQuote?.tokenAddress - Logger.warn("Requested fee token is ", feeTokenAddress) + const feeTokenAddress = tokenPaymasterRequest?.feeQuote?.tokenAddress; + Logger.warn("Requested fee token is ", feeTokenAddress); if (!feeTokenAddress || feeTokenAddress === ADDRESS_ZERO) { throw new Error( - "Invalid or missing token address. Token address must be part of the feeQuote in tokenPaymasterRequest" - ) + "Invalid or missing token address. Token address must be part of the feeQuote in tokenPaymasterRequest", + ); } - const spender = tokenPaymasterRequest?.spender - Logger.warn("Spender address is ", spender) + const spender = tokenPaymasterRequest?.spender; + Logger.warn("Spender address is ", spender); if (!spender || spender === ADDRESS_ZERO) { throw new Error( - "Invalid or missing spender address. Sepnder address must be part of tokenPaymasterRequest" - ) + "Invalid or missing spender address. Sepnder address must be part of tokenPaymasterRequest", + ); } } @@ -1811,19 +1902,19 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { */ async buildTokenPaymasterUserOp( userOp: Partial, - tokenPaymasterRequest: BiconomyTokenPaymasterRequest + tokenPaymasterRequest: BiconomyTokenPaymasterRequest, ): Promise> { - this.validateUserOpAndPaymasterRequest(userOp, tokenPaymasterRequest) + this.validateUserOpAndPaymasterRequest(userOp, tokenPaymasterRequest); try { - let batchTo: Array = [] - let batchValue: Array = [] - let batchData: Array = [] + let batchTo: Array = []; + let batchValue: Array = []; + let batchData: Array = []; - let newCallData = userOp.callData + let newCallData = userOp.callData; Logger.warn( "Received information about fee token address and quote ", - tokenPaymasterRequest.toString() - ) + tokenPaymasterRequest.toString(), + ); if (this.paymaster && this.paymaster instanceof Paymaster) { // Make a call to paymaster.buildTokenApprovalTransaction() with necessary details @@ -1831,57 +1922,57 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { // Review: might request this form of an array of Transaction const approvalRequest: Transaction = await ( this.paymaster as IHybridPaymaster - ).buildTokenApprovalTransaction(tokenPaymasterRequest) - Logger.warn("ApprovalRequest is for erc20 token ", approvalRequest.to) + ).buildTokenApprovalTransaction(tokenPaymasterRequest); + Logger.warn("ApprovalRequest is for erc20 token ", approvalRequest.to); if ( approvalRequest.data === "0x" || approvalRequest.to === ADDRESS_ZERO ) { - return userOp + return userOp; } if (isNullOrUndefined(userOp.callData)) { - throw new Error("UserOp callData cannot be undefined") + throw new Error("UserOp callData cannot be undefined"); } const decodedSmartAccountData = decodeFunctionData({ abi: BiconomyAccountAbi, - data: userOp.callData as Hex - }) + data: userOp.callData as Hex, + }); if (!decodedSmartAccountData) { throw new Error( - "Could not parse userOp call data for this smart account" - ) + "Could not parse userOp call data for this smart account", + ); } const smartAccountExecFunctionName = - decodedSmartAccountData.functionName + decodedSmartAccountData.functionName; Logger.warn( - `Originally an ${smartAccountExecFunctionName} method call for Biconomy Account V2` - ) + `Originally an ${smartAccountExecFunctionName} method call for Biconomy Account V2`, + ); if ( smartAccountExecFunctionName === "execute" || smartAccountExecFunctionName === "execute_ncC" ) { - const methodArgsSmartWalletExecuteCall = decodedSmartAccountData.args - const toOriginal = methodArgsSmartWalletExecuteCall[0] - const valueOriginal = methodArgsSmartWalletExecuteCall[1] - const dataOriginal = methodArgsSmartWalletExecuteCall[2] - - batchTo.push(toOriginal) - batchValue.push(valueOriginal) - batchData.push(dataOriginal) + const methodArgsSmartWalletExecuteCall = decodedSmartAccountData.args; + const toOriginal = methodArgsSmartWalletExecuteCall[0]; + const valueOriginal = methodArgsSmartWalletExecuteCall[1]; + const dataOriginal = methodArgsSmartWalletExecuteCall[2]; + + batchTo.push(toOriginal); + batchValue.push(valueOriginal); + batchData.push(dataOriginal); } else if ( smartAccountExecFunctionName === "executeBatch" || smartAccountExecFunctionName === "executeBatch_y6U" ) { - const methodArgsSmartWalletExecuteCall = decodedSmartAccountData.args - batchTo = [...methodArgsSmartWalletExecuteCall[0]] - batchValue = [...methodArgsSmartWalletExecuteCall[1]] - batchData = [...methodArgsSmartWalletExecuteCall[2]] + const methodArgsSmartWalletExecuteCall = decodedSmartAccountData.args; + batchTo = [...methodArgsSmartWalletExecuteCall[0]]; + batchValue = [...methodArgsSmartWalletExecuteCall[1]]; + batchData = [...methodArgsSmartWalletExecuteCall[2]]; } if ( @@ -1889,52 +1980,52 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { approvalRequest.data && approvalRequest.value ) { - batchTo = [approvalRequest.to as Hex, ...batchTo] + batchTo = [approvalRequest.to as Hex, ...batchTo]; batchValue = [ BigInt(Number(approvalRequest.value.toString())), - ...batchValue - ] - batchData = [approvalRequest.data as Hex, ...batchData] + ...batchValue, + ]; + batchData = [approvalRequest.data as Hex, ...batchData]; newCallData = await this.encodeExecuteBatch( batchTo, batchValue, - batchData - ) + batchData, + ); } const finalUserOp: Partial = { ...userOp, - callData: newCallData - } + callData: newCallData, + }; // Optionally Requesting to update gas limits again (especially callGasLimit needs to be re-calculated) - return finalUserOp + return finalUserOp; } } catch (error) { - Logger.log("Failed to update userOp. Sending back original op") + Logger.log("Failed to update userOp. Sending back original op"); Logger.error( "Failed to update callData with error", - JSON.stringify(error) - ) - return userOp + JSON.stringify(error), + ); + return userOp; } - return userOp + return userOp; } async signUserOpHash(userOpHash: string, params?: ModuleInfo): Promise { - this.isActiveValidationModuleDefined() + this.isActiveValidationModuleDefined(); const moduleSig = (await this.activeValidationModule.signUserOpHash( userOpHash, - params - )) as Hex + params, + )) as Hex; const signatureWithModuleAddress = encodeAbiParameters( parseAbiParameters("bytes, address"), - [moduleSig, this.activeValidationModule.getAddress() as Hex] - ) + [moduleSig, this.activeValidationModule.getAddress() as Hex], + ); - return signatureWithModuleAddress + return signatureWithModuleAddress; } /** @@ -1982,44 +2073,44 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * */ public async deploy( - buildUseropDto?: BuildUserOpOptions + buildUseropDto?: BuildUserOpOptions, ): Promise { const accountAddress = - this.accountAddress ?? (await this.getAccountAddress()) + this.accountAddress ?? (await this.getAccountAddress()); // Check that the account has not already been deployed const byteCode = await this.provider?.getBytecode({ - address: accountAddress as Hex - }) + address: accountAddress as Hex, + }); if (byteCode !== undefined) { - throw new Error(ERROR_MESSAGES.ACCOUNT_ALREADY_DEPLOYED) + throw new Error(ERROR_MESSAGES.ACCOUNT_ALREADY_DEPLOYED); } // Check that the account has enough native token balance to deploy, if not using a paymaster if (!buildUseropDto?.paymasterServiceData?.mode) { const nativeTokenBalance = await this.provider?.getBalance({ - address: accountAddress - }) + address: accountAddress, + }); if (nativeTokenBalance === BigInt(0)) { - throw new Error(ERROR_MESSAGES.NO_NATIVE_TOKEN_BALANCE_DURING_DEPLOY) + throw new Error(ERROR_MESSAGES.NO_NATIVE_TOKEN_BALANCE_DURING_DEPLOY); } } - const useEmptyDeployCallData = true + const useEmptyDeployCallData = true; return this.sendTransaction( { to: accountAddress, - data: "0x" + data: "0x", }, - { ...buildUseropDto, useEmptyDeployCallData } - ) + { ...buildUseropDto, useEmptyDeployCallData }, + ); } async getFactoryData() { - if (await this.isAccountDeployed()) return undefined + if (await this.isAccountDeployed()) return undefined; - this.isDefaultValidationModuleDefined() + this.isDefaultValidationModuleDefined(); return encodeFunctionData({ abi: BiconomyFactoryAbi, @@ -2027,147 +2118,148 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { args: [ this.defaultValidationModule.getAddress() as Hex, (await this.defaultValidationModule.getInitData()) as Hex, - BigInt(this.index) - ] - }) + BigInt(this.index), + ], + }); } async signMessage(message: string | Uint8Array): Promise { - let signature: any - this.isActiveValidationModuleDefined() - const dataHash = typeof message === "string" ? toBytes(message) : message - signature = await this.activeValidationModule.signMessage(dataHash) + // biome-ignore lint/suspicious/noExplicitAny: + let signature: any; + this.isActiveValidationModuleDefined(); + const dataHash = typeof message === "string" ? toBytes(message) : message; + signature = await this.activeValidationModule.signMessage(dataHash); - const potentiallyIncorrectV = Number.parseInt(signature.slice(-2), 16) + const potentiallyIncorrectV = Number.parseInt(signature.slice(-2), 16); if (![27, 28].includes(potentiallyIncorrectV)) { - const correctV = potentiallyIncorrectV + 27 - signature = signature.slice(0, -2) + correctV.toString(16) + const correctV = potentiallyIncorrectV + 27; + signature = signature.slice(0, -2) + correctV.toString(16); } if (signature.slice(0, 2) !== "0x") { - signature = `0x${signature}` + signature = `0x${signature}`; } signature = encodeAbiParameters( [{ type: "bytes" }, { type: "address" }], - [signature as Hex, this.defaultValidationModule.getAddress()] - ) + [signature as Hex, this.defaultValidationModule.getAddress()], + ); if (await this.isAccountDeployed()) { - return signature as Hex + return signature as Hex; } const abiEncodedMessage = encodeAbiParameters( [ { type: "address", - name: "create2Factory" + name: "create2Factory", }, { type: "bytes", - name: "factoryCalldata" + name: "factoryCalldata", }, { type: "bytes", - name: "originalERC1271Signature" - } + name: "originalERC1271Signature", + }, ], [ this.getFactoryAddress() ?? "0x", (await this.getFactoryData()) ?? "0x", - signature - ] - ) - return concat([abiEncodedMessage, MAGIC_BYTES]) + signature, + ], + ); + return concat([abiEncodedMessage, MAGIC_BYTES]); } async getIsValidSignatureData( messageHash: Hex, - signature: Hex + signature: Hex, ): Promise { return encodeFunctionData({ abi: BiconomyAccountAbi, functionName: "isValidSignature", - args: [messageHash, signature] - }) + args: [messageHash, signature], + }); } async enableModule(moduleAddress: Hex): Promise { - const tx: Transaction = await this.getEnableModuleData(moduleAddress) - const partialUserOp = await this.buildUserOp([tx]) - return this.sendUserOp(partialUserOp) + const tx: Transaction = await this.getEnableModuleData(moduleAddress); + const partialUserOp = await this.buildUserOp([tx]); + return this.sendUserOp(partialUserOp); } async getEnableModuleData(moduleAddress: Hex): Promise { const callData = encodeFunctionData({ abi: BiconomyAccountAbi, functionName: "enableModule", - args: [moduleAddress] - }) + args: [moduleAddress], + }); const tx: Transaction = { to: await this.getAddress(), value: "0x00", - data: callData - } - return tx + data: callData, + }; + return tx; } async getSetupAndEnableModuleData( moduleAddress: Hex, - moduleSetupData: Hex + moduleSetupData: Hex, ): Promise { const callData = encodeFunctionData({ abi: BiconomyAccountAbi, functionName: "setupAndEnableModule", - args: [moduleAddress, moduleSetupData] - }) + args: [moduleAddress, moduleSetupData], + }); const tx: Transaction = { to: await this.getAddress(), value: "0x00", - data: callData - } - return tx + data: callData, + }; + return tx; } async disableModule( prevModule: Hex, - moduleAddress: Hex + moduleAddress: Hex, ): Promise { const tx: Transaction = await this.getDisableModuleData( prevModule, - moduleAddress - ) - const partialUserOp = await this.buildUserOp([tx]) - return this.sendUserOp(partialUserOp) + moduleAddress, + ); + const partialUserOp = await this.buildUserOp([tx]); + return this.sendUserOp(partialUserOp); } async getDisableModuleData( prevModule: Hex, - moduleAddress: Hex + moduleAddress: Hex, ): Promise { const callData = encodeFunctionData({ abi: BiconomyAccountAbi, functionName: "disableModule", - args: [prevModule, moduleAddress] - }) + args: [prevModule, moduleAddress], + }); const tx: Transaction = { to: await this.getAddress(), value: "0x00", - data: callData - } - return tx + data: callData, + }; + return tx; } async isModuleEnabled(moduleAddress: Hex): Promise { - const accountContract = await this._getAccountContract() - return accountContract.read.isModuleEnabled([moduleAddress]) + const accountContract = await this._getAccountContract(); + return accountContract.read.isModuleEnabled([moduleAddress]); } // Review async getAllModules(pageSize?: number): Promise> { - const _pageSize = pageSize ?? 100 - const accountContract = await this._getAccountContract() + const _pageSize = pageSize ?? 100; + const accountContract = await this._getAccountContract(); const result = await accountContract.read.getModulesPaginated([ this.SENTINEL_MODULE as Hex, - BigInt(_pageSize) - ]) - const modules: Array = result[0] as Array - return modules + BigInt(_pageSize), + ]); + const modules: Array = result[0] as Array; + return modules; } } diff --git a/src/account/utils/Constants.ts b/src/account/utils/Constants.ts index b568a9ca4..df309580e 100644 --- a/src/account/utils/Constants.ts +++ b/src/account/utils/Constants.ts @@ -90,7 +90,12 @@ export const ERROR_MESSAGES = { "Session indexes and transactions must be of the same length and correspond to each other", SIGNER_REQUIRED: "Signer is required for creating a smart account", UNKNOW_SESSION_ARGUMENTS: - "You have not provided the necessary information to find and use a session" + "You have not provided the necessary information to find and use a session", + CHAIN_ID_MISMATCH: "Chain ID does not match the chain ID of the session", + MISSING_SESSION_ID: "Session ID is missing", + NO_LEAF_FOUND: "No leaf found for the provided session ID", + NO_DAN_MODULE_INFO: "No DAN module info found for the provided session ID", + INVALID_BROWSER_WALLET: "Invalid BrowserWallet provided" } export const NATIVE_TOKEN_ALIAS: Hex = diff --git a/src/account/utils/Types.ts b/src/account/utils/Types.ts index 900b07a93..ebf5a911b 100644 --- a/src/account/utils/Types.ts +++ b/src/account/utils/Types.ts @@ -9,184 +9,190 @@ import type { SignableMessage, TypedData, TypedDataDefinition, - WalletClient -} from "viem" -import type { IBundler } from "../../bundler" -import type { BaseValidationModule, ModuleInfo } from "../../modules" + WalletClient, +} from "viem"; +import type { IBundler } from "../../bundler"; +import type { + BaseValidationModule, + ModuleInfo, + SessionType, +} from "../../modules"; import type { ISessionStorage, - SessionLeafNode -} from "../../modules/interfaces/ISessionStorage" + SessionLeafNode, +} from "../../modules/interfaces/ISessionStorage"; import type { FeeQuotesOrDataDto, IPaymaster, PaymasterFeeQuote, PaymasterMode, SmartAccountData, - SponsorUserOperationDto -} from "../../paymaster" + SponsorUserOperationDto, +} from "../../paymaster"; -export type EntryPointAddresses = Record -export type BiconomyFactories = Record -export type BiconomyImplementations = Record -export type EntryPointAddressesByVersion = Record -export type BiconomyFactoriesByVersion = Record -export type BiconomyImplementationsByVersion = Record +export type EntryPointAddresses = Record; +export type BiconomyFactories = Record; +export type BiconomyImplementations = Record; +export type EntryPointAddressesByVersion = Record; +export type BiconomyFactoriesByVersion = Record; +export type BiconomyImplementationsByVersion = Record; export type SmartAccountConfig = { /** entryPointAddress: address of the entry point */ - entryPointAddress: string + entryPointAddress: string; /** factoryAddress: address of the smart account factory */ - bundler?: IBundler -} + bundler?: IBundler; +}; export interface BalancePayload { /** address: The address of the account */ - address: string + address: string; /** chainId: The chainId of the network */ - chainId: number + chainId: number; /** amount: The amount of the balance */ - amount: bigint + amount: bigint; /** decimals: The number of decimals */ - decimals: number + decimals: number; /** formattedAmount: The amount of the balance formatted */ - formattedAmount: string + formattedAmount: string; } export interface WithdrawalRequest { /** The address of the asset */ - address: Hex + address: Hex; /** The amount to withdraw. Expects unformatted amount. Will use max amount if unset */ - amount?: bigint + amount?: bigint; /** The destination address of the funds. The second argument from the `withdraw(...)` function will be used as the default if left unset. */ - recipient?: Hex + recipient?: Hex; } export interface GasOverheads { /** fixed: fixed gas overhead */ - fixed: number + fixed: number; /** perUserOp: per user operation gas overhead */ - perUserOp: number + perUserOp: number; /** perUserOpWord: per user operation word gas overhead */ - perUserOpWord: number + perUserOpWord: number; /** zeroByte: per byte gas overhead */ - zeroByte: number + zeroByte: number; /** nonZeroByte: per non zero byte gas overhead */ - nonZeroByte: number + nonZeroByte: number; /** bundleSize: per signature bundleSize */ - bundleSize: number + bundleSize: number; /** sigSize: sigSize gas overhead */ - sigSize: number + sigSize: number; } export type BaseSmartAccountConfig = { /** index: helps to not conflict with other smart account instances */ - index?: number + index?: number; /** provider: WalletClientSigner from viem */ - provider?: WalletClient + provider?: WalletClient; /** entryPointAddress: address of the smart account entry point */ - entryPointAddress?: string + entryPointAddress?: string; /** accountAddress: address of the smart account, potentially counterfactual */ - accountAddress?: string + accountAddress?: string; /** overheads: {@link GasOverheads} */ - overheads?: Partial + overheads?: Partial; /** paymaster: {@link IPaymaster} interface */ - paymaster?: IPaymaster + paymaster?: IPaymaster; /** chainId: chainId of the network */ - chainId?: number -} + chainId?: number; +}; export type BiconomyTokenPaymasterRequest = { /** feeQuote: {@link PaymasterFeeQuote} */ - feeQuote: PaymasterFeeQuote + feeQuote: PaymasterFeeQuote; /** spender: The address of the spender who is paying for the transaction, this can usually be set to feeQuotesResponse.tokenPaymasterAddress */ - spender: Hex + spender: Hex; /** maxApproval: If set to true, the paymaster will approve the maximum amount of tokens required for the transaction. Not recommended */ - maxApproval?: boolean + maxApproval?: boolean; /* skip option to patch callData if approval is already given to the paymaster */ - skipPatchCallData?: boolean -} + skipPatchCallData?: boolean; +}; export type RequireAtLeastOne = Pick< T, Exclude > & { - [K in Keys]-?: Required> & Partial>> - }[Keys] + [K in Keys]-?: Required> & Partial>>; + }[Keys]; export type ConditionalBundlerProps = RequireAtLeastOne< { - bundler: IBundler - bundlerUrl: string + bundler: IBundler; + bundlerUrl: string; }, "bundler" | "bundlerUrl" -> +>; export type ResolvedBundlerProps = { - bundler: IBundler -} + bundler: IBundler; +}; export type ConditionalValidationProps = RequireAtLeastOne< { - defaultValidationModule: BaseValidationModule - signer: SupportedSigner + defaultValidationModule: BaseValidationModule; + signer: SupportedSigner; }, "defaultValidationModule" | "signer" -> +>; export type ResolvedValidationProps = { /** defaultValidationModule: {@link BaseValidationModule} */ - defaultValidationModule: BaseValidationModule + defaultValidationModule: BaseValidationModule; /** activeValidationModule: {@link BaseValidationModule}. The active validation module. Will default to the defaultValidationModule */ - activeValidationModule: BaseValidationModule + activeValidationModule: BaseValidationModule; /** signer: ethers Wallet, viemWallet or alchemys SmartAccountSigner */ - signer: SmartAccountSigner + signer: SmartAccountSigner; /** chainId: chainId of the network */ - chainId: number -} + chainId: number; +}; export type BiconomySmartAccountV2ConfigBaseProps = { /** Factory address of biconomy factory contract or some other contract you have deployed on chain */ - factoryAddress?: Hex + factoryAddress?: Hex; /** Sender address: If you want to override the Signer address with some other address and get counterfactual address can use this to pass the EOA and get SA address */ - senderAddress?: Hex + senderAddress?: Hex; /** implementation of smart contract address or some other contract you have deployed and want to override */ - implementationAddress?: Hex + implementationAddress?: Hex; /** defaultFallbackHandler: override the default fallback contract address */ - defaultFallbackHandler?: Hex + defaultFallbackHandler?: Hex; /** rpcUrl: Rpc url, optional, we set default rpc url if not passed. */ - rpcUrl?: string // as good as Provider + rpcUrl?: string; // as good as Provider /** paymasterUrl: The Paymaster URL retrieved from the Biconomy dashboard */ - paymasterUrl?: string + paymasterUrl?: string; /** biconomyPaymasterApiKey: The API key retrieved from the Biconomy dashboard */ - biconomyPaymasterApiKey?: string + biconomyPaymasterApiKey?: string; /** activeValidationModule: The active validation module. Will default to the defaultValidationModule */ - activeValidationModule?: BaseValidationModule + activeValidationModule?: BaseValidationModule; /** scanForUpgradedAccountsFromV1: set to true if you you want the userwho was using biconomy SA v1 to upgrade to biconomy SA v2 */ - scanForUpgradedAccountsFromV1?: boolean + scanForUpgradedAccountsFromV1?: boolean; /** the index of SA the EOA have generated and till which indexes the upgraded SA should scan */ - maxIndexForScan?: number + maxIndexForScan?: number; /** Can be used to optionally override the chain with a custom chain if it doesn't already exist in viems list of supported chains. Alias of customChain */ - viemChain?: Chain + viemChain?: Chain; /** Can be used to optionally override the chain with a custom chain if it doesn't already exist in viems list of supported chain. Alias of viemChain */ - customChain?: Chain + customChain?: Chain; /** The initial code to be used for the smart account */ - initCode?: Hex + initCode?: Hex; /** Used for session key manager module */ - sessionData?: ModuleInfo + sessionData?: ModuleInfo; /** Used to skip the chain checks between singer, bundler and paymaster */ - skipChainCheck?: boolean -} + skipChainCheck?: boolean; + /** The type of the relevant session. Used with createSessionSmartAccountClient */ + sessionType?: SessionType; +}; export type BiconomySmartAccountV2Config = BiconomySmartAccountV2ConfigBaseProps & BaseSmartAccountConfig & ConditionalBundlerProps & - ConditionalValidationProps + ConditionalValidationProps; export type BiconomySmartAccountV2ConfigConstructorProps = BiconomySmartAccountV2ConfigBaseProps & BaseSmartAccountConfig & ResolvedBundlerProps & - ResolvedValidationProps + ResolvedValidationProps; /** * Represents options for building a user operation. @@ -201,30 +207,30 @@ export type BiconomySmartAccountV2ConfigConstructorProps = * @property {boolean} [useEmptyDeployCallData] - Set to true if the transaction is being used only to deploy the smart contract, so "0x" is set as the user operation call data. */ export type BuildUserOpOptions = { - gasOffset?: GasOffsetPct - params?: ModuleInfo - nonceOptions?: NonceOptions - forceEncodeForBatch?: boolean - paymasterServiceData?: PaymasterUserOperationDto - simulationType?: SimulationType - stateOverrideSet?: StateOverrideSet - dummyPndOverride?: BytesLike - useEmptyDeployCallData?: boolean -} + gasOffset?: GasOffsetPct; + params?: ModuleInfo; + nonceOptions?: NonceOptions; + forceEncodeForBatch?: boolean; + paymasterServiceData?: PaymasterUserOperationDto; + simulationType?: SimulationType; + stateOverrideSet?: StateOverrideSet; + dummyPndOverride?: BytesLike; + useEmptyDeployCallData?: boolean; +}; export type SessionDataForAccount = { - sessionStorageClient: ISessionStorage - session: SessionLeafNode -} + sessionStorageClient: ISessionStorage; + session: SessionLeafNode; +}; export type NonceOptions = { /** nonceKey: The key to use for nonce */ - nonceKey?: number + nonceKey?: number; /** nonceOverride: The nonce to use for the transaction */ - nonceOverride?: number -} + nonceOverride?: number; +}; -export type SimulationType = "validation" | "validation_and_execution" +export type SimulationType = "validation" | "validation_and_execution"; /** * Represents an offset percentage value used for gas-related calculations. @@ -245,177 +251,177 @@ export type SimulationType = "validation" | "validation_and_execution" * @property {number} [maxPriorityFeePerGasOffsetPct] - Percentage offset for the maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas). */ export type GasOffsetPct = { - callGasLimitOffsetPct?: number - verificationGasLimitOffsetPct?: number - preVerificationGasOffsetPct?: number - maxFeePerGasOffsetPct?: number - maxPriorityFeePerGasOffsetPct?: number -} + callGasLimitOffsetPct?: number; + verificationGasLimitOffsetPct?: number; + preVerificationGasOffsetPct?: number; + maxFeePerGasOffsetPct?: number; + maxPriorityFeePerGasOffsetPct?: number; +}; export type InitilizationData = { - accountIndex?: number - signerAddress?: string -} + accountIndex?: number; + signerAddress?: string; +}; export type PaymasterUserOperationDto = SponsorUserOperationDto & FeeQuotesOrDataDto & { /** mode: sponsored or erc20 */ - mode: PaymasterMode + mode: PaymasterMode; /** Always recommended, especially when using token paymaster */ - calculateGasLimits?: boolean + calculateGasLimits?: boolean; /** Expiry duration in seconds */ - expiryDuration?: number + expiryDuration?: number; /** Webhooks to be fired after user op is sent */ // biome-ignore lint/suspicious/noExplicitAny: - webhookData?: Record + webhookData?: Record; /** Smart account meta data */ - smartAccountInfo?: SmartAccountData + smartAccountInfo?: SmartAccountData; /** the fee-paying token address */ - feeTokenAddress?: string + feeTokenAddress?: string; /** The fee quote */ - feeQuote?: PaymasterFeeQuote + feeQuote?: PaymasterFeeQuote; /** The address of the spender. This is usually set to FeeQuotesOrDataResponse.tokenPaymasterAddress */ - spender?: Hex + spender?: Hex; /** Not recommended */ - maxApproval?: boolean + maxApproval?: boolean; /* skip option to patch callData if approval is already given to the paymaster */ - skipPatchCallData?: boolean - } + skipPatchCallData?: boolean; + }; export type InitializeV2Data = { - accountIndex?: number -} + accountIndex?: number; +}; export type EstimateUserOpGasParams = { - userOp: Partial + userOp: Partial; /** Currrently has no effect */ // skipBundlerGasEstimation?: boolean; /** paymasterServiceData: Options specific to transactions that involve a paymaster */ - paymasterServiceData?: SponsorUserOperationDto -} + paymasterServiceData?: SponsorUserOperationDto; +}; export interface TransactionDetailsForUserOp { /** target: The address of the contract to call */ - target: string + target: string; /** data: The data to send to the contract */ - data: string + data: string; /** value: The value to send to the contract */ - value?: BigNumberish + value?: BigNumberish; /** gasLimit: The gas limit to use for the transaction */ - gasLimit?: BigNumberish + gasLimit?: BigNumberish; /** maxFeePerGas: The maximum fee per gas to use for the transaction */ - maxFeePerGas?: BigNumberish + maxFeePerGas?: BigNumberish; /** maxPriorityFeePerGas: The maximum priority fee per gas to use for the transaction */ - maxPriorityFeePerGas?: BigNumberish + maxPriorityFeePerGas?: BigNumberish; /** nonce: The nonce to use for the transaction */ - nonce?: BigNumberish + nonce?: BigNumberish; } export type CounterFactualAddressParam = { - index?: number - validationModule?: BaseValidationModule + index?: number; + validationModule?: BaseValidationModule; /** scanForUpgradedAccountsFromV1: set to true if you you want the userwho was using biconomy SA v1 to upgrade to biconomy SA v2 */ - scanForUpgradedAccountsFromV1?: boolean + scanForUpgradedAccountsFromV1?: boolean; /** the index of SA the EOA have generated and till which indexes the upgraded SA should scan */ - maxIndexForScan?: number -} + maxIndexForScan?: number; +}; export type QueryParamsForAddressResolver = { - eoaAddress: Hex - index: number - moduleAddress: Hex - moduleSetupData: Hex - maxIndexForScan?: number -} + eoaAddress: Hex; + index: number; + moduleAddress: Hex; + moduleSetupData: Hex; + maxIndexForScan?: number; +}; export type SmartAccountInfo = { /** accountAddress: The address of the smart account */ - accountAddress: Hex + accountAddress: Hex; /** factoryAddress: The address of the smart account factory */ - factoryAddress: Hex + factoryAddress: Hex; /** currentImplementation: The address of the current implementation */ - currentImplementation: string + currentImplementation: string; /** currentVersion: The version of the smart account */ - currentVersion: string + currentVersion: string; /** factoryVersion: The version of the factory */ - factoryVersion: string + factoryVersion: string; /** deploymentIndex: The index of the deployment */ - deploymentIndex: BigNumberish -} + deploymentIndex: BigNumberish; +}; export type ValueOrData = RequireAtLeastOne< { - value: BigNumberish | string - data: string + value: BigNumberish | string; + data: string; }, "value" | "data" -> +>; export type Transaction = { - to: string -} & ValueOrData + to: string; +} & ValueOrData; export type SupportedToken = Omit< PaymasterFeeQuote, "maxGasFeeUSD" | "usdPayment" | "maxGasFee" | "validUntil" -> & { balance: BalancePayload } +> & { balance: BalancePayload }; export type Signer = LightSigner & { // biome-ignore lint/suspicious/noExplicitAny: any is used here to allow for the ethers provider - provider: any -} -export type SupportedSignerName = "alchemy" | "ethers" | "viem" + provider: any; +}; +export type SupportedSignerName = "alchemy" | "ethers" | "viem"; export type SupportedSigner = | SmartAccountSigner | WalletClient | Signer | LightSigner - | PrivateKeyAccount -export type Service = "Bundler" | "Paymaster" + | PrivateKeyAccount; +export type Service = "Bundler" | "Paymaster"; export interface LightSigner { - getAddress(): Promise - signMessage(message: string | Uint8Array): Promise + getAddress(): Promise; + signMessage(message: string | Uint8Array): Promise; } export type StateOverrideSet = { [key: string]: { - balance?: string - nonce?: string - code?: string - state?: object - stateDiff?: object - } -} + balance?: string; + nonce?: string; + code?: string; + state?: object; + stateDiff?: object; + }; +}; -export type BigNumberish = Hex | number | bigint -export type BytesLike = Uint8Array | Hex +export type BigNumberish = Hex | number | bigint; +export type BytesLike = Uint8Array | Hex; //#region UserOperationStruct // based on @account-abstraction/common // this is used for building requests export interface UserOperationStruct { /* the origin of the request */ - sender: string + sender: string; /* nonce of the transaction, returned from the entry point for this Address */ - nonce: BigNumberish + nonce: BigNumberish; /* the initCode for creating the sender if it does not exist yet, otherwise "0x" */ - initCode: BytesLike | "0x" + initCode: BytesLike | "0x"; /* the callData passed to the target */ - callData: BytesLike + callData: BytesLike; /* Value used by inner account execution */ - callGasLimit?: BigNumberish + callGasLimit?: BigNumberish; /* Actual gas used by the validation of this UserOperation */ - verificationGasLimit?: BigNumberish + verificationGasLimit?: BigNumberish; /* Gas overhead of this UserOperation */ - preVerificationGas?: BigNumberish + preVerificationGas?: BigNumberish; /* Maximum fee per gas (similar to EIP-1559 max_fee_per_gas) */ - maxFeePerGas?: BigNumberish + maxFeePerGas?: BigNumberish; /* Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas) */ - maxPriorityFeePerGas?: BigNumberish + maxPriorityFeePerGas?: BigNumberish; /* Address of paymaster sponsoring the transaction, followed by extra data to send to the paymaster ("0x" for self-sponsored transaction) */ - paymasterAndData: BytesLike | "0x" + paymasterAndData: BytesLike | "0x"; /* Data passed into the account along with the nonce during the verification step */ - signature: BytesLike + signature: BytesLike; } //#endregion UserOperationStruct @@ -435,19 +441,19 @@ export interface UserOperationStruct { // biome-ignore lint/suspicious/noExplicitAny: export interface SmartAccountSigner { - signerType: string - inner: Inner + signerType: string; + inner: Inner; - getAddress: () => Promise
+ getAddress: () => Promise
; - signMessage: (message: SignableMessage) => Promise + signMessage: (message: SignableMessage) => Promise; signTypedData: < const TTypedData extends TypedData | { [key: string]: unknown }, - TPrimaryType extends string = string + TPrimaryType extends string = string, >( - params: TypedDataDefinition - ) => Promise + params: TypedDataDefinition, + ) => Promise; } //#endregion SmartAccountSigner @@ -455,47 +461,47 @@ export interface SmartAccountSigner { export type UserOperationCallData = | { /* the target of the call */ - target: Address + target: Address; /* the data passed to the target */ - data: Hex + data: Hex; /* the amount of native token to send to the target (default: 0) */ - value?: bigint + value?: bigint; } - | Hex + | Hex; //#endregion UserOperationCallData //#region BatchUserOperationCallData -export type BatchUserOperationCallData = Exclude[] +export type BatchUserOperationCallData = Exclude[]; //#endregion BatchUserOperationCallData -export type SignTypedDataParams = Omit +export type SignTypedDataParams = Omit; export type BasSmartContractAccountProps = BiconomySmartAccountV2ConfigConstructorProps & { /** chain: The chain from viem */ - chain: Chain + chain: Chain; /** rpcClient: The rpc url string */ - rpcClient: string + rpcClient: string; /** factoryAddress: The address of the factory */ - factoryAddress: Hex + factoryAddress: Hex; /** entryPointAddress: The address of the entry point */ - entryPointAddress: Hex + entryPointAddress: Hex; /** accountAddress: The address of the account */ - accountAddress?: Address - } + accountAddress?: Address; + }; export interface ISmartContractAccount< - TSigner extends SmartAccountSigner = SmartAccountSigner + TSigner extends SmartAccountSigner = SmartAccountSigner, > { /** * The RPC provider the account uses to make RPC calls */ - readonly rpcProvider: PublicClient + readonly rpcProvider: PublicClient; /** * @returns the init code for the account */ - getInitCode(): Promise + getInitCode(): Promise; /** * This is useful for estimating gas costs. It should return a signature that doesn't cause the account to revert @@ -503,7 +509,7 @@ export interface ISmartContractAccount< * * @returns a dummy signature that doesn't cause the account to revert during estimation */ - getDummySignature(): Hex + getDummySignature(): Hex; /** * Encodes a call to the account's execute function. @@ -512,7 +518,7 @@ export interface ISmartContractAccount< * @param value - optionally the amount of native token to send * @param data - the call data or "0x" if empty */ - encodeExecute(target: string, value: bigint, data: string): Promise + encodeExecute(target: string, value: bigint, data: string): Promise; /** * Encodes a batch of transactions to the account's batch execute function. @@ -520,12 +526,12 @@ export interface ISmartContractAccount< * @param txs - An Array of objects containing the target, value, and data for each transaction * @returns the encoded callData for a UserOperation */ - encodeBatchExecute(txs: BatchUserOperationCallData): Promise + encodeBatchExecute(txs: BatchUserOperationCallData): Promise; /** * @returns the nonce of the account */ - getNonce(): Promise + getNonce(): Promise; /** * If your account handles 1271 signatures of personal_sign differently @@ -534,7 +540,7 @@ export interface ISmartContractAccount< * @param uoHash -- The hash of the UserOperation to sign * @returns the signature of the UserOperation */ - signUserOperationHash(uoHash: Hash): Promise + signUserOperationHash(uoHash: Hash): Promise; /** * Returns a signed and prefixed message. @@ -542,7 +548,7 @@ export interface ISmartContractAccount< * @param msg - the message to sign * @returns the signature of the message */ - signMessage(msg: string | Uint8Array | Hex): Promise + signMessage(msg: string | Uint8Array | Hex): Promise; /** * Signs a typed data object as per ERC-712 @@ -550,7 +556,7 @@ export interface ISmartContractAccount< * @param params - {@link SignTypedDataParams} * @returns the signed hash for the message passed */ - signTypedData(params: SignTypedDataParams): Promise + signTypedData(params: SignTypedDataParams): Promise; /** * If the account is not deployed, it will sign the message and then wrap it in 6492 format @@ -558,7 +564,7 @@ export interface ISmartContractAccount< * @param msg - the message to sign * @returns ths signature wrapped in 6492 format */ - signMessageWith6492(msg: string | Uint8Array | Hex): Promise + signMessageWith6492(msg: string | Uint8Array | Hex): Promise; /** * If the account is not deployed, it will sign the typed data blob and then wrap it in 6492 format @@ -566,12 +572,12 @@ export interface ISmartContractAccount< * @param params - {@link SignTypedDataParams} * @returns the signed hash for the params passed in wrapped in 6492 format */ - signTypedDataWith6492(params: SignTypedDataParams): Promise + signTypedDataWith6492(params: SignTypedDataParams): Promise; /** * @returns the address of the account */ - getAddress(): Promise
+ getAddress(): Promise
; /** * @returns the current account signer instance that the smart account client @@ -580,17 +586,17 @@ export interface ISmartContractAccount< * The signer is expected to be the owner or one of the owners of the account * for the signatures to be valid for the acting account. */ - getSigner(): TSigner + getSigner(): TSigner; /** * @returns the address of the factory contract for the smart account */ - getFactoryAddress(): Address + getFactoryAddress(): Address; /** * @returns the address of the entry point contract for the smart account */ - getEntryPointAddress(): Address + getEntryPointAddress(): Address; /** * Allows you to add additional functionality and utility methods to this account @@ -617,14 +623,14 @@ export interface ISmartContractAccount< * with the extension methods * @returns -- the account with the extension methods added */ - extend: (extendFn: (self: this) => R) => this & R + extend: (extendFn: (self: this) => R) => this & R; encodeUpgradeToAndCall: ( upgradeToImplAddress: Address, - upgradeToInitData: Hex - ) => Promise + upgradeToInitData: Hex, + ) => Promise; } export type TransferOwnershipCompatibleModule = | "0x0000001c5b32F37F5beA87BDD5374eB2aC54eA8e" - | "0x000000824dc138db84FD9109fc154bdad332Aa8E" + | "0x000000824dc138db84FD9109fc154bdad332Aa8E"; diff --git a/src/account/utils/convertSigner.ts b/src/account/utils/convertSigner.ts index 96bb0f7f5..a4a2180ab 100644 --- a/src/account/utils/convertSigner.ts +++ b/src/account/utils/convertSigner.ts @@ -20,7 +20,9 @@ function isPrivateKeyAccount( return (signer as PrivateKeyAccount).type === "local" } -function isWalletClient(signer: SupportedSigner): signer is WalletClient { +export function isWalletClient( + signer: SupportedSigner +): signer is WalletClient { return (signer as WalletClient).transport !== undefined } diff --git a/src/account/utils/getChain.ts b/src/account/utils/getChain.ts index 0ccb8be7e..c87c7dc29 100644 --- a/src/account/utils/getChain.ts +++ b/src/account/utils/getChain.ts @@ -122,5 +122,6 @@ export const getCustomChain = ( }, ...((contracts && { contracts }) || {}) } + CUSTOM_CHAINS.push(chain) return chain } diff --git a/src/bundler/utils/Constants.ts b/src/bundler/utils/Constants.ts index 5c530f57d..7c0510526 100644 --- a/src/bundler/utils/Constants.ts +++ b/src/bundler/utils/Constants.ts @@ -28,5 +28,3 @@ export const UserOpWaitForTxHashMaxDurationIntervals: { export const DEFAULT_ENTRYPOINT_ADDRESS = "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789" - -export const SDK_VERSION = "4.4.5" diff --git a/src/bundler/utils/getAAError.ts b/src/bundler/utils/getAAError.ts index 2f0425b3c..762124cc8 100644 --- a/src/bundler/utils/getAAError.ts +++ b/src/bundler/utils/getAAError.ts @@ -1,6 +1,4 @@ -import { BaseError } from "viem" import type { Service } from "../../account" -import { SDK_VERSION } from "./Constants" export type KnownError = { name: string regex: string @@ -39,21 +37,6 @@ const buildErrorStrings = ( service ? `\nSent via: ${service}` : "" ].filter(Boolean) -type AccountAbstractionErrorParams = { - docsSlug?: string - metaMessages?: string[] - details?: string -} - -class AccountAbstractionError extends BaseError { - override name = "AccountAbstractionError" - override version = `@biconomy/account@${SDK_VERSION}` - - constructor(title: string, params: AccountAbstractionErrorParams = {}) { - super(title, params) - } -} - export const getAAError = async ( message: string, httpStatus?: number, @@ -78,9 +61,5 @@ export const getAAError = async ( const title = matchedError ? matchedError.name : "Unknown Error" const docsSlug = matchedError?.docsUrl ?? DOCS_URL - return new AccountAbstractionError(title, { - docsSlug, - metaMessages, - details - }) + return new Error([title, status, docsSlug, metaMessages, details].join("\n")) } diff --git a/src/modules/index.ts b/src/modules/index.ts index b31dc4981..f0032eaf0 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -12,8 +12,10 @@ export * from "./session-validation-modules/ERC20SessionValidationModule.js" export * from "./sessions/abi.js" export * from "./sessions/erc20.js" export * from "./sessions/batch.js" +export * from "./sessions/dan.js" export * from "./sessions/sessionSmartAccountClient.js" export * from "./session-storage/index.js" +export * from "./walletprovider-sdk/types.js" import { BatchedSessionRouterModule, ECDSAOwnershipValidationModule, @@ -21,7 +23,6 @@ import { MultiChainValidationModule, SessionKeyManagerModule } from "./index.js" - export const createBatchedSessionRouterModule = BatchedSessionRouterModule.create export const createMultiChainValidationModule = @@ -31,4 +32,6 @@ export const createECDSAOwnershipValidationModule = export const createSessionKeyManagerModule = SessionKeyManagerModule.create export const createERC20SessionValidationModule = ERC20SessionValidationModule.create -// export * from './PasskeyValidationModule' +// @ts-ignore +export * as WalletProviderSDK from "./walletprovider-sdk/index.js" + diff --git a/src/modules/interfaces/ISessionStorage.ts b/src/modules/interfaces/ISessionStorage.ts index 61fc64de3..61600fb0c 100644 --- a/src/modules/interfaces/ISessionStorage.ts +++ b/src/modules/interfaces/ISessionStorage.ts @@ -1,6 +1,6 @@ import type { Chain, Hex } from "viem" import type { SmartAccountSigner } from "../../account" -import type { SignerData } from "../utils/Types.js" +import type { DanModuleInfo, SignerData } from "../utils/Types.js" export type SessionStatus = | "PENDING" @@ -17,6 +17,7 @@ export type SessionLeafNode = { sessionPublicKey: Hex sessionID?: string status: SessionStatus + danModuleInfo?: DanModuleInfo } export type SessionSearchParam = { diff --git a/src/modules/session-storage/SessionLocalStorage.ts b/src/modules/session-storage/SessionLocalStorage.ts index fdb6de08c..eea4f10c6 100644 --- a/src/modules/session-storage/SessionLocalStorage.ts +++ b/src/modules/session-storage/SessionLocalStorage.ts @@ -10,9 +10,11 @@ import type { } from "../interfaces/ISessionStorage.js" import type { SignerData } from "../utils/Types.js" +// @ts-ignore +export const inBrowser = typeof window !== "undefined" export const supportsLocalStorage = // @ts-ignore: LocalStorage is not available in node - typeof window !== "undefined" && typeof window.localStorage !== "undefined" + inBrowser && typeof window.localStorage !== "undefined" export class SessionLocalStorage implements ISessionStorage { public smartAccountAddress: Hex diff --git a/src/modules/session-storage/SessionMemoryStorage.ts b/src/modules/session-storage/SessionMemoryStorage.ts index 4b79174a1..635737010 100644 --- a/src/modules/session-storage/SessionMemoryStorage.ts +++ b/src/modules/session-storage/SessionMemoryStorage.ts @@ -200,23 +200,6 @@ export class SessionMemoryStorage implements ISessionStorage { return newLeafNodes } - // async revokeSession( - // sessionID: string - // ): Promise { - // let data = this.getSessionStore() - // const oldRoot = await this.getMerkleRoot(); - // console.log(oldRoot, "oldRoot"); - // const updatedSession = data.leafNodes.filter((s: SessionLeafNode) => s.sessionID !== sessionID) - // data.leafNodes = updatedSession; - // memoryStorage.setItem(this.getStorageKey("sessions"), JSON.stringify(data)) - // const newSessions = this.getSessionStore() - // console.log(newSessions, "newSessions"); - // const newMerkleRoot = await this.getMerkleRoot(); - // console.log(newMerkleRoot, "newMerkleRoot"); - // await this.setMerkleRoot(newMerkleRoot) - // return newMerkleRoot; - // } - async getSignerBySession( param: SessionSearchParam, chain: Chain diff --git a/src/modules/sessions/abi.ts b/src/modules/sessions/abi.ts index efad26cd0..5d61a517e 100644 --- a/src/modules/sessions/abi.ts +++ b/src/modules/sessions/abi.ts @@ -9,30 +9,36 @@ import { toFunctionSelector, toHex } from "viem" +import { + createSessionKeyManagerModule, + didProvideFullSession, + resumeSession +} from "../" + import type { CreateSessionDataParams, - Permission, + DanModuleInfo, SessionParams, - UserOpResponse -} from "../../" +} from "../utils/Types" +import type { CreateSessionWithDistributedKeyParams } from "./dan" + import { type BiconomySmartAccountV2, type BuildUserOpOptions, ERROR_MESSAGES, Logger, - type Transaction + type Transaction, + getChain } from "../../account" -import { - createSessionKeyManagerModule, - didProvideFullSession, - resumeSession -} from "../index" +import type { UserOpResponse } from "../../bundler/utils/Types" +import { extractChainIdFromBundlerUrl } from "../../bundler/utils/Utils" import type { ISessionStorage } from "../interfaces/ISessionStorage" +import { createSessionKeyEOA } from "../session-storage/utils" import { DEFAULT_ABI_SVM_MODULE, DEFAULT_SESSION_KEY_MANAGER_MODULE } from "../utils/Constants" -import type { SessionSearchParam } from "../utils/Helper" +import type { Permission, SessionSearchParam } from "../utils/Helper" import type { DeprecatedPermission, Rule } from "../utils/Helper" export type SessionConfig = { @@ -54,6 +60,29 @@ export type SessionEpoch = { validAfter?: number } +export const PolicyHelpers = { + Indefinitely: { validUntil: 0, validAfter: 0 }, + NoValueLimit: 0n +} +const RULE_CONDITIONS = [ + "EQUAL", + "LASS_THAN_OR_EQUAL", + "LESS_THAN", + "GREATER_THAN_OR_EQUAL", + "GREATER_THAN", + "NOT_EQUAL" +] as const + +export type RuleCondition = typeof RULE_CONDITIONS[number] +export const RuleHelpers = { + OffsetByIndex: (i: number): number => i * 32, + Condition: (condition: RuleCondition): number => RULE_CONDITIONS.map(r => r.toUpperCase()).indexOf(condition.toUpperCase()) +} + +export type PolicyWithOptionalSessionKey = Omit & { + sessionKeyAddress?: Hex +} + export type Policy = { /** The address of the contract to be included in the policy */ contractAddress: Hex @@ -85,6 +114,8 @@ export type SessionGrantedPayload = UserOpResponse & { session: Session } * @param policy - An array of session configurations {@link Policy}. * @param sessionStorageClient - The storage client to store the session keys. {@link ISessionStorage} * @param buildUseropDto - Optional. {@link BuildUserOpOptions} + * @param storeSessionKeyInDAN - Optional. If true, the session key stored on the DAN network. Must be used with "DISTRIBUTED_KEY" {@link SessionType} when creating the sessionSmartAccountClient and using the session + * @param browserWallet - Optional. The browser wallet instance. Only relevant when storeSessionKeyInDan is true. {@link CreateSessionWithDistributedKeyParams['browserWallet']} * @returns Promise<{@link SessionGrantedPayload}> - An object containing the status of the transaction and the sessionID. * * @example @@ -144,18 +175,40 @@ export type SessionGrantedPayload = UserOpResponse & { session: Session } */ export const createSession = async ( smartAccount: BiconomySmartAccountV2, - policy: Policy[], - sessionStorageClient: ISessionStorage, - buildUseropDto?: BuildUserOpOptions + policy: PolicyWithOptionalSessionKey[], + sessionStorageClient?: ISessionStorage | null, + buildUseropDto?: BuildUserOpOptions, ): Promise => { + const smartAccountAddress = await smartAccount.getAddress() + const defaultedChainId = extractChainIdFromBundlerUrl( + smartAccount?.bundler?.getBundlerUrl() ?? "" + ) + + if (!defaultedChainId) { + throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND) + } + + const chain = getChain(defaultedChainId) + const { + sessionKeyAddress, + sessionStorageClient: storageClientFromCreateKey + } = await createSessionKeyEOA(smartAccount, chain) + + const defaultedSessionStorageClient = + sessionStorageClient ?? storageClientFromCreateKey const sessionsModule = await createSessionKeyManagerModule({ smartAccountAddress, - sessionStorageClient + sessionStorageClient: defaultedSessionStorageClient }) + const defaultedPolicy: Policy[] = policy.map((p) => + !p.sessionKeyAddress ? { ...p, sessionKeyAddress } : (p as Policy) + ) + const humanReadablePolicyArray = defaultedPolicy.map(createABISessionDatum) + const { data: policyData, sessionIDInfo } = - await sessionsModule.createSessionData(policy.map(createABISessionDatum)) + await sessionsModule.createSessionData(humanReadablePolicyArray) const permitTx = { to: DEFAULT_SESSION_KEY_MANAGER_MODULE, @@ -187,7 +240,7 @@ export const createSession = async ( return { session: { - sessionStorageClient, + sessionStorageClient: defaultedSessionStorageClient, sessionIDInfo }, ...userOpResponse @@ -205,6 +258,7 @@ export type CreateSessionDatumParams = { functionSelector: string | AbiFunction | HardcodedFunctionSelector rules: Rule[] valueLimit: bigint + danModuleInfo?: DanModuleInfo } /** @@ -229,7 +283,9 @@ export const createABISessionDatum = ({ /** The rules to be included in the policy */ rules, /** The maximum value that can be transferred in a single transaction */ - valueLimit + valueLimit, + /** information pertinent to the DAN module */ + danModuleInfo }: CreateSessionDatumParams): CreateSessionDataParams => { const { validUntil = 0, validAfter = 0 } = interval ?? {} @@ -250,7 +306,7 @@ export const createABISessionDatum = ({ ) } - return { + const result = { validUntil, validAfter, sessionValidationModule: DEFAULT_ABI_SVM_MODULE, @@ -262,6 +318,8 @@ export const createABISessionDatum = ({ rules }) } + + return danModuleInfo ? { ...result, danModuleInfo } : result } /** diff --git a/src/modules/sessions/dan.ts b/src/modules/sessions/dan.ts new file mode 100644 index 000000000..0193d9bab --- /dev/null +++ b/src/modules/sessions/dan.ts @@ -0,0 +1,337 @@ +import { getPublicKeyAsync } from "@noble/ed25519" +import type { Chain, Hex } from "viem" +import { generatePrivateKey } from "viem/accounts" +import { type Session, createDANSessionKeyManagerModule } from "../" +import { + type BiconomySmartAccountV2, + type BuildUserOpOptions, + ERROR_MESSAGES, + Logger, + type Transaction, + isWalletClient +} from "../../account" +import { extractChainIdFromBundlerUrl } from "../../bundler" +import { WalletProviderSDK } from "../index" +import type { ISessionStorage } from "../interfaces/ISessionStorage" +import { getDefaultStorageClient } from "../session-storage/utils" +import { + DAN_BACKEND_URL, + DEFAULT_SESSION_KEY_MANAGER_MODULE +} from "../utils/Constants" +import { + NodeWallet, + type SessionSearchParam, + computeAddress, + didProvideFullSession, + hexToUint8Array, + resumeSession +} from "../utils/Helper" +import type { DanModuleInfo } from "../utils/Types" +import type { IBrowserWallet } from "../walletprovider-sdk/types" +import { + type Policy, + type SessionGrantedPayload, + createABISessionDatum +} from "./abi" + +export type PolicyLeaf = Omit +export const DEFAULT_SESSION_DURATION = 60 * 60 + +export type CreateDistributedParams = { + /** The user's smart account instance */ + smartAccountClient: BiconomySmartAccountV2, + /** An array of session configurations */ + policy: PolicyLeaf[], + /** The storage client to store the session keys */ + sessionStorageClient?: ISessionStorage, + /** The build userop dto */ + buildUseropDto?: BuildUserOpOptions, + /** The chain ID */ + chainId?: number, + /** Optional. The user's {@link IBrowserWallet} instance. Default will be the signer associated with the smart account. */ + browserWallet?: IBrowserWallet +} + +/** + * + * createDistributedSession + * + * Creates a session for a user's smart account. + * This grants a dapp permission to execute a specific function on a specific contract on behalf of a user. + * Permissions can be specified by the dapp in the form of rules{@link Rule}, and then submitted to the user for approval via signing. + * The session keys granted with the imparted policy are stored in a StorageClient {@link ISessionStorage}. They can later be retrieved and used to validate userops. + * + * @param smartAccount - The user's {@link BiconomySmartAccountV2} smartAccount instance. + * @param policy - An array of session configurations {@link Policy}. + * @param sessionStorageClient - The storage client to store the session keys. {@link ISessionStorage} + * @param buildUseropDto - Optional. {@link BuildUserOpOptions} + * @param chainId - Optional. Will be inferred if left unset. + * @param browserWallet - Optional. The user's {@link IBrowserWallet} instance. Default will be the signer associated with the smart account. + * @returns Promise<{@link SessionGrantedPayload}> - An object containing the status of the transaction and the sessionID. + * + * @example + * + * import { type PolicyLeaf, type Session, createDistributedSession } from "@biconomy/account" + * + * const policy: PolicyLeaf[] = [{ + * contractAddress: nftAddress, + * functionSelector: "safeMint(address)", + * rules: [ + * { + * offset: 0, + * condition: 0, + * referenceValue: smartAccountAddress + * } + * ], + * interval: { + * validUntil: 0, + * validAfter: 0 + * }, + * valueLimit: 0n + * }] + * + * const { wait, session } = await createDistributedSession({ + * smartAccountClient, + * policy + * }) + * + * const { success } = await wait() +*/ +export const createDistributedSession = async ({ + smartAccountClient, + policy, + sessionStorageClient, + buildUseropDto, + chainId, + browserWallet +}: CreateDistributedParams): Promise => { + const defaultedChainId = + chainId ?? + extractChainIdFromBundlerUrl(smartAccountClient?.bundler?.getBundlerUrl() ?? ""); + + if (!defaultedChainId) { + throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND) + } + const smartAccountAddress = await smartAccountClient.getAddress() + const defaultedSessionStorageClient = + sessionStorageClient || getDefaultStorageClient(smartAccountAddress) + + const sessionsModule = await createDANSessionKeyManagerModule({ + smartAccountAddress, + sessionStorageClient: defaultedSessionStorageClient + }) + + let duration = DEFAULT_SESSION_DURATION + if (policy?.[0].interval?.validUntil) { + duration = Math.round(policy?.[0].interval?.validUntil - Date.now() / 1000) + } + + const { sessionKeyEOA: sessionKeyAddress, ...other } = await getDANSessionKey({ + smartAccountClient: smartAccount, + browserWallet, + duration + }) + + const danModuleInfo: DanModuleInfo = { ...other } + const defaultedPolicy: Policy[] = policy.map((p) => ({ + ...p, + sessionKeyAddress + })) + + const humanReadablePolicyArray = defaultedPolicy.map((p) => + createABISessionDatum({ ...p, danModuleInfo }) + ) + + const { data: policyData, sessionIDInfo } = + await sessionsModule.createSessionData(humanReadablePolicyArray) + + const permitTx = { + to: DEFAULT_SESSION_KEY_MANAGER_MODULE, + data: policyData + } + + const txs: Transaction[] = [] + + const isDeployed = await smartAccountClient.isAccountDeployed() + const enableSessionTx = await smartAccountClient.getEnableModuleData( + DEFAULT_SESSION_KEY_MANAGER_MODULE + ) + + if (isDeployed) { + const enabled = await smartAccountClient.isModuleEnabled( + DEFAULT_SESSION_KEY_MANAGER_MODULE + ) + if (!enabled) { + txs.push(enableSessionTx) + } + } else { + Logger.log(ERROR_MESSAGES.ACCOUNT_NOT_DEPLOYED) + txs.push(enableSessionTx) + } + + txs.push(permitTx) + + const userOpResponse = await smartAccountClient.sendTransaction(txs, buildUseropDto) + + smartAccountClient.setActiveValidationModule(sessionsModule) + + return { + session: { + sessionStorageClient: defaultedSessionStorageClient, + sessionIDInfo + }, + ...userOpResponse + } +} + +export type DanSessionKeyPayload = { + /** Dan Session ephemeral key*/ + sessionKeyEOA: Hex; + /** Dan Session MPC key ID*/ + mpcKeyId: string; + /** Dan Session ephemeral private key without 0x prefi x*/ + hexEphSKWithout0x: string; + /** Number of nodes that participate in keygen operation. Also known as n. */ + partiesNumber: number; + /** Number of nodes that needs to participate in protocol in order to generate valid signature. Also known as t. */ + threshold: number; + /** The eoa that was used to create the session */ + eoaAddress: Hex; + /** the chainId is relevant only to the */ + chainId: number +} + +export type DanSessionKeyRequestParams = { + /** Relevant smart account */ + smartAccountClient: BiconomySmartAccountV2; + /** Optional browser wallet. If using wagmi can be set to connector.getProvider() from useAccount hook */ + browserWallet?: IBrowserWallet; + /** Optional hardcoded values if required */ + hardcodedValues?: Partial; + /** Optional duration of the session key in seconds. Default is 3600 seconds. */ + duration?: number; + /** Optional chainId. Will be inferred if left unset. */ + chain?: Chain; +} + +/** + * + * getDANSessionKey + * + * @description This function is used to generate a new session key for a Distributed Account Network (DAN) session. This information is kept in the session storage and can be used to validate userops without the user's direct involvement. + * + * Generates a new session key for a Distributed Account Network (DAN) session. + * @param smartAccount - The user's {@link BiconomySmartAccountV2} smartAccount instance. + * @param browserWallet - Optional. The user's {@link IBrowserWallet} instance. + * @param hardcodedValues - Optional. {@link DanModuleInfo} - Additional information for the DAN module configuration to override the default values. + * @param duration - Optional. The duration of the session key in seconds. Default is 3600 seconds. + * @param chain - Optional. The chain ID. Will be inferred if left unset. + * @returns Promise<{@link DanModuleInfo}> - An object containing the session key, the MPC key ID, the number of parties, the threshold, and the EOA address. + * +*/ +export const getDANSessionKey = async ({ + smartAccountClient, + browserWallet, + hardcodedValues = {}, + duration = DEFAULT_SESSION_DURATION, + chain +}: DanSessionKeyRequestParams): Promise => { + + const eoaAddress = hardcodedValues?.eoaAddress ?? (await smartAccountClient.getSigner().getAddress()) as Hex // Smart account owner + const innerSigner = smartAccountClient.getSigner().inner + + const defaultedChainId = chain?.id ?? extractChainIdFromBundlerUrl( + smartAccountClient?.bundler?.getBundlerUrl() ?? "" + ) + + if (!defaultedChainId) { + throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND) + } + + if (!browserWallet && !isWalletClient(innerSigner)) + throw new Error(ERROR_MESSAGES.INVALID_BROWSER_WALLET) + const wallet = browserWallet ?? new NodeWallet(innerSigner) + + const hexEphSK = generatePrivateKey() + const hexEphSKWithout0x = hardcodedValues?.hexEphSKWithout0x ?? hexEphSK.slice(2) + + const ephSK: Uint8Array = hexToUint8Array(hexEphSKWithout0x) + const ephPK: Uint8Array = await getPublicKeyAsync(ephSK); + + const wpClient = new WalletProviderSDK.WalletProviderServiceClient({ + walletProviderId: "WalletProvider", + walletProviderUrl: DAN_BACKEND_URL + }) + + const eoaAuth = new WalletProviderSDK.EOAAuth(eoaAddress, wallet, ephPK, duration); + + const partiesNumber = hardcodedValues?.partiesNumber ?? 20 + const threshold = hardcodedValues?.threshold ?? 11 + + const sdk = new WalletProviderSDK.NetworkSigner(wpClient, threshold, partiesNumber, eoaAuth) + + // @ts-ignore + const resp = await sdk.authenticateAndCreateKey(ephPK) + + const pubKey = resp.publicKey + const mpcKeyId = resp.keyId as Hex + + const sessionKeyEOA = computeAddress(pubKey) + + return { + sessionKeyEOA, + mpcKeyId, + hexEphSKWithout0x, + partiesNumber, + threshold, + eoaAddress, + chainId: defaultedChainId + } +} + +export type DanSessionParamsPayload = { + params: { + sessionID: string + } +} +/** + * getDanSessionTxParams + * + * Retrieves the transaction parameters for a batched session. + * + * @param correspondingIndex - An index for the transaction corresponding to the relevant session. If not provided, the last session index is used. + * @param conditionalSession - {@link SessionSearchParam} The session data that contains the sessionID and sessionSigner. If not provided, The default session storage (localStorage in browser, fileStorage in node backend) is used to fetch the sessionIDInfo + * @returns Promise<{@link DanSessionParamsPayload}> - session parameters. + * + */ +export const getDanSessionTxParams = async ( + conditionalSession: SessionSearchParam, + chain: Chain, + correspondingIndex?: number | null | undefined +): Promise => { + const defaultedCorrespondingIndex = Array.isArray(correspondingIndex) + ? correspondingIndex[0] + : correspondingIndex + const resumedSession = await resumeSession(conditionalSession) + // if correspondingIndex is null then use the last session. + const allSessions = + await resumedSession.sessionStorageClient.getAllSessionData() + + const sessionID = didProvideFullSession(conditionalSession) + ? (conditionalSession as Session).sessionIDInfo[ + defaultedCorrespondingIndex ?? 0 + ] + : allSessions[defaultedCorrespondingIndex ?? allSessions.length - 1] + .sessionID + + const matchingLeafDatum = allSessions.find((s) => s.sessionID === sessionID) + + if (!sessionID) throw new Error(ERROR_MESSAGES.MISSING_SESSION_ID) + if (!matchingLeafDatum) throw new Error(ERROR_MESSAGES.NO_LEAF_FOUND) + if (!matchingLeafDatum.danModuleInfo) + throw new Error(ERROR_MESSAGES.NO_DAN_MODULE_INFO) + const chainIdsMatch = chain.id === matchingLeafDatum?.danModuleInfo?.chainId + if (!chainIdsMatch) throw new Error(ERROR_MESSAGES.CHAIN_ID_MISMATCH) + + return { params: { sessionID } } +} diff --git a/src/modules/sessions/sessionSmartAccountClient.ts b/src/modules/sessions/sessionSmartAccountClient.ts index 0b8137ab3..0a657d604 100644 --- a/src/modules/sessions/sessionSmartAccountClient.ts +++ b/src/modules/sessions/sessionSmartAccountClient.ts @@ -1,30 +1,44 @@ -import { http, type Chain, type Hex, createWalletClient } from "viem" -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { http, type Chain, type Hex, createWalletClient } from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { type BiconomySmartAccountV2, type BiconomySmartAccountV2Config, type BuildUserOpOptions, type SupportedSigner, + type Transaction, createSmartAccountClient, - getChain -} from "../../account" + getChain, +} from "../../account"; +import type { UserOpResponse } from "../../bundler/index.js"; import { type SessionSearchParam, createBatchedSessionRouterModule, createSessionKeyManagerModule, - resumeSession -} from "../index.js" -import type { ISessionStorage } from "../interfaces/ISessionStorage" -import type { ModuleInfo, StrictSessionParams } from "../utils/Types" + type getSingleSessionTxParams, + resumeSession, +} from "../index.js"; +import type { ISessionStorage } from "../interfaces/ISessionStorage"; +import type { ModuleInfo, StrictSessionParams } from "../utils/Types"; +export type SessionType = "STANDARD" | "BATCHED" | "DISTRIBUTED_KEY"; export type ImpersonatedSmartAccountConfig = Omit< BiconomySmartAccountV2Config, "signer" > & { - accountAddress: Hex - chainId: number - bundlerUrl: string -} + accountAddress: Hex; + chainId: number; + bundlerUrl: string; +}; + +export type GetSessionParameters = Parameters; +export type GetSessionResponse = { params: ModuleInfo }; + +export type SendSessionTransactionFunction = ( + getParameters: GetSessionParameters, + manyOrOneTransactions: Transaction | Transaction[], + buildUseropDto?: BuildUserOpOptions, +) => Promise; + /** * * createSessionSmartAccountClient @@ -35,7 +49,7 @@ export type ImpersonatedSmartAccountConfig = Omit< * * @param biconomySmartAccountConfig - Configuration for initializing the BiconomySmartAccountV2 instance {@link ImpersonatedSmartAccountConfig}. * @param conditionalSession - {@link SessionSearchParam} The session data that contains the sessionID and sessionSigner. If not provided, The default session storage (localStorage in browser, fileStorage in node backend) is used to fetch the sessionIDInfo - * @param multiMode - If true, the smart account instance will use the batchedSessionModule for validation, otherwise a single session is assumed. + * @param sessionType - {@link SessionType}: One of "STANDARD", "BATCHED" or "DISTRIBUTED_KEY". Default is "STANDARD". * @returns A promise that resolves to a new instance of {@link BiconomySmartAccountV2}. * @throws An error if something is wrong with the smart account instance creation. * @@ -66,7 +80,7 @@ export type ImpersonatedSmartAccountConfig = Omit< * paymasterUrl, * chainId * }, - * storeForSingleSession // Can be ommitted if using default session storage (localStorage in browser, fileStorage in node backend) + * "DEFAULT_STORE" // Can be ommitted if using default session storage (localStorage in browser, fileStorage in node backend) * ) * * // The smartAccountWithSession instance can now be used to interact with the blockchain on behalf of the user in the same manner as a regular smart account instance. @@ -75,58 +89,76 @@ export type ImpersonatedSmartAccountConfig = Omit< */ export const createSessionSmartAccountClient = async ( biconomySmartAccountConfig: ImpersonatedSmartAccountConfig, - conditionalSession: SessionSearchParam, - multiMode = false + conditionalSession: SessionSearchParam | "DEFAULT_STORE", + sessionType?: SessionType | boolean, // boolean for backwards compatibility ): Promise => { - const { sessionStorageClient, sessionIDInfo } = await resumeSession( - conditionalSession ?? biconomySmartAccountConfig.accountAddress - ) - const sessionID = sessionIDInfo[0] // Default to the first element to find the signer + // for backwards compatibility + let defaultedSessionType: SessionType = "STANDARD"; + if (sessionType === true || sessionType === "BATCHED") + defaultedSessionType = "BATCHED"; + if (sessionType === "DISTRIBUTED_KEY") defaultedSessionType = "DISTRIBUTED_KEY"; - const account = privateKeyToAccount(generatePrivateKey()) + const defaultedSessionStore = conditionalSession !== "DEFAULT_STORE" ? conditionalSession : biconomySmartAccountConfig.accountAddress; + const { sessionStorageClient, sessionIDInfo } = await resumeSession(defaultedSessionStore); + const account = privateKeyToAccount(generatePrivateKey()); const chain = biconomySmartAccountConfig.viemChain ?? biconomySmartAccountConfig.customChain ?? - getChain(biconomySmartAccountConfig.chainId) + getChain(biconomySmartAccountConfig.chainId); const incompatibleSigner = createWalletClient({ account, chain, - transport: http() - }) - - const sessionSigner = await sessionStorageClient.getSignerBySession( - { - sessionID - }, - chain - ) + transport: http(), + }); - const sessionData: ModuleInfo | undefined = multiMode - ? undefined - : { + // Obselete & flow removed from docs but will keep for backwards compatibility reasons. + let sessionData: ModuleInfo | undefined; + try { + const sessionID = sessionIDInfo?.[0]; // Default to the first element to find the signer + const sessionSigner = await sessionStorageClient.getSignerBySession( + { sessionID, - sessionSigner - } + }, + chain, + ); + + sessionData = + defaultedSessionType === "STANDARD" + ? { + sessionID, + sessionSigner, + } + : undefined; + } catch (e) { } const sessionModule = await createSessionKeyManagerModule({ smartAccountAddress: biconomySmartAccountConfig.accountAddress, - sessionStorageClient - }) + sessionStorageClient, + }); - const batchedSessionModule = await createBatchedSessionRouterModule({ - smartAccountAddress: biconomySmartAccountConfig.accountAddress, - sessionKeyManagerModule: sessionModule - }) + const batchedSessionValidationModule = await createBatchedSessionRouterModule( + { + smartAccountAddress: biconomySmartAccountConfig.accountAddress, + sessionKeyManagerModule: sessionModule, + }, + ); + + const activeValidationModule = + defaultedSessionType === "BATCHED" + ? batchedSessionValidationModule + : sessionModule return await createSmartAccountClient({ ...biconomySmartAccountConfig, signer: incompatibleSigner, // This is a dummy signer, it will remain unused - activeValidationModule: multiMode ? batchedSessionModule : sessionModule, - sessionData // contains the sessionSigner that will be used for txs - }) -} + activeValidationModule, + sessionData, + sessionType: defaultedSessionType, + sessionStorageClient, + }); +}; /** * @@ -136,18 +168,18 @@ export const createSessionSmartAccountClient = async ( */ export const toSupportedSigner = ( privateKey: string, - chain: Chain + chain: Chain, ): SupportedSigner => { const parsedPrivateKey: Hex = privateKey.startsWith("0x") ? (privateKey as Hex) - : `0x${privateKey}` - const account = privateKeyToAccount(parsedPrivateKey) + : `0x${privateKey}`; + const account = privateKeyToAccount(parsedPrivateKey); return createWalletClient({ account, chain, - transport: http() - }) -} + transport: http(), + }); +}; /** * @@ -159,9 +191,9 @@ export const toSupportedSigner = ( export const toSessionParams = ( privateKey: Hex, sessionIDs: string[], - chain: Chain + chain: Chain, ): StrictSessionParams[] => sessionIDs.map((sessionID) => ({ sessionID, - sessionSigner: toSupportedSigner(privateKey, chain) - })) + sessionSigner: toSupportedSigner(privateKey, chain), + })); diff --git a/src/modules/utils/Constants.ts b/src/modules/utils/Constants.ts index 36ff89ca9..42d308f55 100644 --- a/src/modules/utils/Constants.ts +++ b/src/modules/utils/Constants.ts @@ -37,3 +37,5 @@ export const MULTICHAIN_VALIDATION_MODULE_ADDRESSES_BY_VERSION = { export const DEFAULT_ABI_SVM_MODULE: Hex = "0x000006bC2eCdAe38113929293d241Cf252D91861" + +export const DAN_BACKEND_URL = "wss://dan.staging.biconomy.io/v1" diff --git a/src/modules/utils/Helper.ts b/src/modules/utils/Helper.ts index ba884e9e4..116eb0b3c 100644 --- a/src/modules/utils/Helper.ts +++ b/src/modules/utils/Helper.ts @@ -1,14 +1,21 @@ +import { ProjectivePoint } from "@noble/secp256k1" import { type Address, type ByteArray, type Chain, + type EIP1193Provider, type Hex, + type WalletClient, encodeAbiParameters, isAddress, keccak256, parseAbiParameters } from "viem" -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { + generatePrivateKey, + privateKeyToAccount, + publicKeyToAddress +} from "viem/accounts" import { ERROR_MESSAGES, type UserOperationStruct, @@ -18,11 +25,11 @@ import type { ChainInfo, HardcodedReference, Session, - SignerData + SignerData, } from "../../index.js" import type { ISessionStorage } from "../interfaces/ISessionStorage" import { getDefaultStorageClient } from "../session-storage/utils" - +import type { IBrowserWallet, TypedData } from "../walletprovider-sdk/types.js" /** * Rule * @@ -62,12 +69,12 @@ export interface Rule { condition: number /** The value to compare against */ referenceValue: - | string - | number - | bigint - | boolean - | ByteArray - | HardcodedReference + | string + | number + | bigint + | boolean + | ByteArray + | HardcodedReference } /** @@ -185,7 +192,7 @@ export const parseChain = (chainInfo: ChainInfo): Chain => { * When a smart account address is provided, the default session storage client is used to reconstruct the session object using **all** of the sessionIds found in the storage client * */ -export type SessionSearchParam = Session | ISessionStorage | Address +export type SessionSearchParam = Session | ISessionStorage | Address | "DEFAULT_STORE" export const didProvideFullSession = ( searchParam: SessionSearchParam ): boolean => !!(searchParam as Session)?.sessionIDInfo?.length @@ -237,3 +244,71 @@ export const resumeSession = async ( } throw new Error(ERROR_MESSAGES.UNKNOW_SESSION_ARGUMENTS) } + +export const hexToUint8Array = (hex: string) => { + if (hex.length % 2 !== 0) { + throw new Error("Hex string must have an even number of characters") + } + const array = new Uint8Array(hex.length / 2) + for (let i = 0; i < hex.length; i += 2) { + array[i / 2] = Number.parseInt(hex.substr(i, 2), 16) + } + return array +} + +export const computeAddress = (_publicKey: string): Address => { + let publicKey = _publicKey + + if (publicKey.startsWith("0x")) { + publicKey = publicKey.slice(2) + } + + if (publicKey.startsWith("04")) { + return publicKeyToAddress(`0x${publicKey} `) + } + + if (publicKey.startsWith("02") || publicKey.startsWith("03")) { + const uncompressed = ProjectivePoint.fromHex(publicKey).toHex(false) + return publicKeyToAddress(`0x${uncompressed}`) + } + + throw new Error("Invalid public key") +} + +// Sign data using the secret key stored on Browser Wallet +// It creates a popup window, presenting the human readable form of `request` +// Throws an error if User rejected signature +export class BrowserWallet implements IBrowserWallet { + provider: EIP1193Provider + + constructor(provider: EIP1193Provider) { + this.provider = provider + } + + async signTypedData( + from: string, + request: TypedData + ): Promise { + return await this.provider.request({ + method: "eth_signTypedData_v4", + // @ts-ignore + params: [from, JSON.stringify(request)] + }) + } +} + +// Sign data using the secret key stored on Browser Wallet +// It creates a popup window, presenting the human readable form of `request` +// Throws an error if User rejected signature +export class NodeWallet implements IBrowserWallet { + walletClient: WalletClient + + constructor(walletClient: WalletClient) { + this.walletClient = walletClient + } + + async signTypedData(_: string, request: TypedData): Promise { + // @ts-ignore + return await this.walletClient.signTypedData(request) + } +} diff --git a/src/modules/utils/Types.ts b/src/modules/utils/Types.ts index 414dbeca0..0c569a1de 100644 --- a/src/modules/utils/Types.ts +++ b/src/modules/utils/Types.ts @@ -1,5 +1,6 @@ import type { Chain, Hex } from "viem" import type { + BytesLike, SimulationType, SmartAccountSigner, SupportedSigner, @@ -92,6 +93,23 @@ export type StrictSessionParams = { sessionSigner: SupportedSigner } +export type DanModuleInfo = { + /** Ephemeral sk */ + hexEphSKWithout0x: string + /** eoa address */ + eoaAddress: Hex + /** threshold */ + threshold: number + /** parties number */ + partiesNumber: number + /** userOp to be signed */ + userOperation?: Partial + /** chainId */ + chainId: number + /** selected mpc key id */ + mpcKeyId: string +} + export type ModuleInfo = { // Could be a full object of below params and that way it can be an array too! // sessionParams?: SessionParams[] // where SessionParams is below four @@ -104,6 +122,8 @@ export type ModuleInfo = { additionalSessionData?: string /** Batch session params */ batchSessionParams?: SessionParams[] + /** Dan module info */ + danModuleInfo?: DanModuleInfo } export interface SendUserOpParams extends ModuleInfo { @@ -138,6 +158,8 @@ export interface CreateSessionDataParams { sessionKeyData: Hex /** we generate uuid based sessionId. but if you prefer to track it on your side and attach custom session identifier this can be passed */ preferredSessionId?: string + /** Dan module info */ + danModuleInfo?: DanModuleInfo } export interface MultiChainValidationModuleConfig @@ -185,3 +207,12 @@ export interface SessionValidationModuleConfig { /** Address of the module */ moduleAddress: string } + +export interface DanSignatureObject { + userOperation: Partial & { + initCode: BytesLike | undefined + } + entryPointVersion: string + entryPointAddress: string + chainId: number +} \ No newline at end of file diff --git a/src/modules/walletprovider-sdk/EOAauthentication.ts b/src/modules/walletprovider-sdk/EOAauthentication.ts new file mode 100644 index 000000000..22fa5adfc --- /dev/null +++ b/src/modules/walletprovider-sdk/EOAauthentication.ts @@ -0,0 +1,73 @@ +import type { TypedDataDomain } from 'viem'; +import type { UserAuthentication } from './authentication'; +/** Externally Owned Account (EOA) atuhentication. Uses secret key stored on a wallet to sign requests. + * The requests are presented to the user in a readable form by using TypedData (EIP712). + */ +import type { KeygenSetupOpts, SignSetupOpts } from './networkSigner'; +export type EphClaim = { + eoa: string; + ephPK: string; + expiry: number; +}; +export type FieldDefinition = { + name: string; + type: string; +}; +/** EIP-712 Typed data struct definition. + * @alpha + * */ +export type TypedData = { + /** contains the schema definition of the types that are in `msg` */ + types: Record>; + /** is the signature domain separator */ + domain: TypedDataDomain; + /** points to the type from `types`. It's the root object of `message` */ + primaryType: string; + /** the request that User is asked to sign */ + message: T; +}; +/** + * Interface to implement communication between this library, and a Browser Wallet. In order to + * request the signature from the User. + * @alpha + */ +export interface IBrowserWallet { + /** Sign data using the secret key stored on Browser Wallet + * It creates a popup window, presenting the human readable form of `request` + * @param from - the address used to sign the request + * @param request - the request to sign by the User in the form of EIP712 typed data. + * @throws Throws an error if User rejected signature + * @example The example implementation: + * ```ts + * async signTypedData(from: string, request: TypedData): Promise { + * return await browserWallet.request({ + * method: 'eth_signTypedData_v4', + * params: [from, JSON.stringify(request)], + * }); + * } + * ``` + */ + signTypedData(from: string, request: TypedData): Promise; +} +/** Present the request to the User using wallet UI, and ask for sign. + * The signature is the authorization for the operation + */ +export declare function authenticateUsingEOA({ setup, user_id, challenge, browserWallet, ephPK, lifetime, }: { + setup: KeygenSetupOpts; + user_id: string; + challenge: string; + browserWallet: IBrowserWallet; + ephPK: Uint8Array; + lifetime: number; +}): Promise; +/** Present the request to the User using wallet UI, and ask for sign. + * The signature is the authorization for the operation + */ +export declare function authenticateUsingEphKey({ setup, user_id, challenge, ephSK, ephPK, }: { + setup: SignSetupOpts; + user_id: string; + challenge: string; + ephSK: Uint8Array; + ephPK: Uint8Array; +}): Promise; +//# sourceMappingURL=EOAauthentication.d.ts.map \ No newline at end of file diff --git a/src/modules/walletprovider-sdk/authentication.ts b/src/modules/walletprovider-sdk/authentication.ts new file mode 100644 index 000000000..9911eeaa8 --- /dev/null +++ b/src/modules/walletprovider-sdk/authentication.ts @@ -0,0 +1,70 @@ +import type { IBrowserWallet } from './EOAauthentication'; +import type { KeygenSetupOpts, SignSetupOpts } from './networkSigner'; +/** Type of the request authentication + * @alpha + */ +export declare enum AuthMethod { + /** Authentication using Externally Owned Account */ + EOA = 0, + /** No authentication */ + NONE = 1 +} +export type UserCredentials = { + id: string; + method: string; + credentials: string; +}; +export type UserAuthentication = { + credentials: UserCredentials; + signature: string; +}; +export interface AuthModule { + authenticate({ setup, challenge, }: { + setup: KeygenSetupOpts | SignSetupOpts; + challenge: string; + }): Promise; +} +/** The `AuthModule` implementing Externally Owned Account authentication. + * @alpha + */ +export declare class EOAAuth implements AuthModule { + /** User ID, typically the ETH address that is used to do authentication */ + userId: string; + /** An interface to the wallet, like MetaMask, that is used to sign the requests */ + browserWallet: IBrowserWallet; + /** Public key of the ephemeral key */ + ephPK: Uint8Array; + /** Lifetime of the ephemeral key */ + lifetime: number; + constructor(userId: string, browserWallet: IBrowserWallet, ephPK: Uint8Array, lifetime?: number); + /** + * Prepares a message to present on the Browser Wallet window and requests to sign it. + * @param setup - either Keygen or Sign setup options + * @param challenge - the challenge received from the backend + * + * @public + */ + authenticate({ setup, challenge, }: { + setup: KeygenSetupOpts | SignSetupOpts; + challenge: string; + }): Promise; +} +/** An Ephmeral key used to locally sign the signature requests to network. + * This eph key is registered during keygen. The key is used to sign the requests without + * asking the user to sign the request each time. + * The auth module is only used for signing requests to the network. + * */ +export declare class EphAuth implements AuthModule { + /** User ID, typically the ETH address that is used to do authentication */ + userId: string; + /** Secret key of the ephemeral keypair */ + ephSK: Uint8Array; + /** Public key of the ephemeral keypair */ + ephPK: Uint8Array; + constructor(userId: string, ephSK: Uint8Array); + authenticate({ setup, challenge, }: { + setup: KeygenSetupOpts | SignSetupOpts; + challenge: string; + }): Promise; +} +//# sourceMappingURL=authentication.d.ts.map \ No newline at end of file diff --git a/src/modules/walletprovider-sdk/encoding.ts b/src/modules/walletprovider-sdk/encoding.ts new file mode 100644 index 000000000..60d3b6379 --- /dev/null +++ b/src/modules/walletprovider-sdk/encoding.ts @@ -0,0 +1,5 @@ +export declare const decodeHex: (s: string) => Uint8Array; +export declare const encodeHex: (a: Uint8Array) => string; +export declare const decodeBase64: (b64: string) => Uint8Array; +export declare const encodeBase64: (b: Uint8Array) => string; +//# sourceMappingURL=encoding.d.ts.map \ No newline at end of file diff --git a/src/modules/walletprovider-sdk/index.ts b/src/modules/walletprovider-sdk/index.ts new file mode 100644 index 000000000..c61b2329f --- /dev/null +++ b/src/modules/walletprovider-sdk/index.ts @@ -0,0 +1,2 @@ +// @ts-nocheck +var z = ($, Q) => () => ($ && (Q = $($ = 0)), Q); function L0($, { strict: Q = !0 } = {}) { if (!$) return !1; if (typeof $ !== "string") return !1; return Q ? /^0x[0-9a-fA-F]*$/.test($) : $.startsWith("0x") } var E0 = z(() => { }); function N0($) { if (L0($, { strict: !1 })) return Math.ceil(($.length - 2) / 2); return $.length } var H0 = z(() => { E0() }); var f0; var B0 = z(() => { f0 = "2.11.1" }); var A0; var P0 = z(() => { B0(); A0 = () => `viem@${f0}` }); class P extends Error { constructor($, Q = {}) { super(); Object.defineProperty(this, "details", { enumerable: !0, configurable: !0, writable: !0, value: void 0 }), Object.defineProperty(this, "docsPath", { enumerable: !0, configurable: !0, writable: !0, value: void 0 }), Object.defineProperty(this, "metaMessages", { enumerable: !0, configurable: !0, writable: !0, value: void 0 }), Object.defineProperty(this, "shortMessage", { enumerable: !0, configurable: !0, writable: !0, value: void 0 }), Object.defineProperty(this, "name", { enumerable: !0, configurable: !0, writable: !0, value: "ViemError" }), Object.defineProperty(this, "version", { enumerable: !0, configurable: !0, writable: !0, value: A0() }); const J = Q.cause instanceof P ? Q.cause.details : Q.cause?.message ? Q.cause.message : Q.details, q = Q.cause instanceof P ? Q.cause.docsPath || Q.docsPath : Q.docsPath; if (this.message = [$ || "An error occurred.", "", ...Q.metaMessages ? [...Q.metaMessages, ""] : [], ...q ? [`Docs: https://viem.sh${q}${Q.docsSlug ? `#${Q.docsSlug}` : ""}`] : [], ...J ? [`Details: ${J}`] : [], `Version: ${this.version}`].join("\n"), Q.cause) this.cause = Q.cause; this.details = J, this.docsPath = q, this.metaMessages = Q.metaMessages, this.shortMessage = $ } walk($) { return _0(this, $) } } var _0; var X0 = z(() => { P0(); _0 = ($, Q) => { if (Q?.($)) return $; if ($ && typeof $ === "object" && "cause" in $) return _0($.cause, Q); return Q ? null : $ } }); class h extends P { constructor({ size: $, targetSize: Q, type: J }) { super(`${J.charAt(0).toUpperCase()}${J.slice(1).toLowerCase()} size (${$}) exceeds padding size (${Q}).`); Object.defineProperty(this, "name", { enumerable: !0, configurable: !0, writable: !0, value: "SizeExceedsPaddingSizeError" }) } } var S0 = z(() => { X0() }); function r($, { dir: Q, size: J = 32 } = {}) { if (typeof $ === "string") return O8($, { dir: Q, size: J }); return R8($, { dir: Q, size: J }) } function O8($, { dir: Q, size: J = 32 } = {}) { if (J === null) return $; const q = $.replace("0x", ""); if (q.length > J * 2) throw new h({ size: Math.ceil(q.length / 2), targetSize: J, type: "hex" }); return `0x${q[Q === "right" ? "padEnd" : "padStart"](J * 2, "0")}` } function R8($, { dir: Q, size: J = 32 } = {}) { if (J === null) return $; if ($.length > J) throw new h({ size: $.length, targetSize: J, type: "bytes" }); const q = new Uint8Array(J); for (let N = 0; N < J; N++) { const X = Q === "right"; q[X ? N : J - N - 1] = $[X ? N : $.length - N - 1] } return q } var y0 = z(() => { S0() }); class Y0 extends P { constructor({ max: $, min: Q, signed: J, size: q, value: N }) { super(`Number "${N}" is not in safe ${q ? `${q * 8}-bit ${J ? "signed" : "unsigned"} ` : ""}integer range ${$ ? `(${Q} to ${$})` : `(above ${Q})`}`); Object.defineProperty(this, "name", { enumerable: !0, configurable: !0, writable: !0, value: "IntegerOutOfRangeError" }) } } class Z0 extends P { constructor({ givenSize: $, maxSize: Q }) { super(`Size cannot exceed ${Q} bytes. Given size: ${$} bytes.`); Object.defineProperty(this, "name", { enumerable: !0, configurable: !0, writable: !0, value: "SizeOverflowError" }) } } var I0 = z(() => { X0() }); function V0($, { size: Q }) { if (N0($) > Q) throw new Z0({ givenSize: N0($), maxSize: Q }) } var b0 = z(() => { I0(); H0() }); function _($, Q = {}) { if (typeof $ === "number" || typeof $ === "bigint") return v0($, Q); if (typeof $ === "string") return g0($, Q); if (typeof $ === "boolean") return x0($, Q); return T0($, Q) } function x0($, Q = {}) { const J = `0x${Number($)}`; if (typeof Q.size === "number") return V0(J, { size: Q.size }), r(J, { size: Q.size }); return J } function T0($, Q = {}) { let J = ""; for (let N = 0; N < $.length; N++)J += G8[$[N]]; const q = `0x${J}`; if (typeof Q.size === "number") return V0(q, { size: Q.size }), r(q, { dir: "right", size: Q.size }); return q } function v0($, Q = {}) { const { signed: J, size: q } = Q, N = BigInt($); let X; if (q) if (J) X = (1n << BigInt(q) * 8n - 1n) - 1n; else X = 2n ** (BigInt(q) * 8n) - 1n; else if (typeof $ === "number") X = BigInt(Number.MAX_SAFE_INTEGER); const Y = typeof X === "bigint" && J ? -X - 1n : 0; if (X && N > X || N < Y) { const T = typeof $ === "bigint" ? "n" : ""; throw new Y0({ max: X ? `${X}${T}` : void 0, min: `${Y}${T}`, signed: J, size: q, value: `${$}${T}` }) } const Z = `0x${(J && N < 0 ? (1n << BigInt(q * 8)) + BigInt(N) : N).toString(16)}`; if (q) return r(Z, { size: q }); return Z } function g0($, Q = {}) { const J = K8.encode($); return T0(J, Q) } var G8, K8; var c0 = z(() => { I0(); y0(); b0(); G8 = Array.from({ length: 256 }, ($, Q) => Q.toString(16).padStart(2, "0")), K8 = new TextEncoder }); var j8, F0, m0; var d0 = z(() => { j8 = ($, ...Q) => { if (!($ instanceof Uint8Array)) throw new Error("Expected Uint8Array"); if (Q.length > 0 && !Q.includes($.length)) throw new Error(`Expected Uint8Array of length ${Q}, not of length=${$.length}`) }, F0 = ($, Q = !0) => { if ($.destroyed) throw new Error("Hash instance has been destroyed"); if (Q && $.finished) throw new Error("Hash#digest() has already been called") }, m0 = ($, Q) => { j8($); const J = Q.outputLen; if ($.length < J) throw new Error(`digestInto() expects output buffer of length at least ${J}`) } }); var u0, W8, b8, o, D0, z8, C8, w8, L8, E8, H8, f8, B8, A8, P8, _8, S8, y8, x8, v8, g8, c8, m8, d8, u8, F; var p0 = z(() => { u0 = ($, Q = !1) => { if (Q) return { h: Number($ & o), l: Number($ >> D0 & o) }; return { h: Number($ >> D0 & o) | 0, l: Number($ & o) | 0 } }, W8 = ($, Q = !1) => { const J = new Uint32Array($.length), q = new Uint32Array($.length); for (let N = 0; N < $.length; N++) { const { h: X, l: Y } = u0($[N], Q);[J[N], q[N]] = [X, Y] } return [J, q] }, b8 = ($, Q, J, q) => { const N = (Q >>> 0) + (q >>> 0); return { h: $ + J + (N / 4294967296 | 0) | 0, l: N | 0 } }, o = BigInt(4294967295), D0 = BigInt(32), z8 = ($, Q) => BigInt($ >>> 0) << D0 | BigInt(Q >>> 0), C8 = ($, Q, J) => $ >>> J, w8 = ($, Q, J) => $ << 32 - J | Q >>> J, L8 = ($, Q, J) => $ >>> J | Q << 32 - J, E8 = ($, Q, J) => $ << 32 - J | Q >>> J, H8 = ($, Q, J) => $ << 64 - J | Q >>> J - 32, f8 = ($, Q, J) => $ >>> J - 32 | Q << 64 - J, B8 = ($, Q) => Q, A8 = ($, Q) => $, P8 = ($, Q, J) => $ << J | Q >>> 32 - J, _8 = ($, Q, J) => Q << J | $ >>> 32 - J, S8 = ($, Q, J) => Q << J - 32 | $ >>> 64 - J, y8 = ($, Q, J) => $ << J - 32 | Q >>> 64 - J, x8 = ($, Q, J) => ($ >>> 0) + (Q >>> 0) + (J >>> 0), v8 = ($, Q, J, q) => Q + J + q + ($ / 4294967296 | 0) | 0, g8 = ($, Q, J, q) => ($ >>> 0) + (Q >>> 0) + (J >>> 0) + (q >>> 0), c8 = ($, Q, J, q, N) => Q + J + q + N + ($ / 4294967296 | 0) | 0, m8 = ($, Q, J, q, N) => ($ >>> 0) + (Q >>> 0) + (J >>> 0) + (q >>> 0) + (N >>> 0), d8 = ($, Q, J, q, N, X) => Q + J + q + N + X + ($ / 4294967296 | 0) | 0, u8 = { fromBig: u0, split: W8, toBig: z8, shrSH: C8, shrSL: w8, rotrSH: L8, rotrSL: E8, rotrBH: H8, rotrBL: f8, rotr32H: B8, rotr32L: A8, rotlSH: P8, rotlSL: _8, rotlBH: S8, rotlBL: y8, add: b8, add3L: x8, add3H: v8, add4L: g8, add4H: c8, add5H: d8, add5L: m8 }, F = u8 }); function i8($) { if (typeof $ !== "string") throw new Error(`utf8ToBytes expected string, got ${typeof $}`); return new Uint8Array((new TextEncoder()).encode($)) } function U0($) { if (typeof $ === "string") $ = i8($); if (!p8($)) throw new Error(`expected Uint8Array, got ${typeof $}`); return $ } function n0($) { const Q = (q) => $().update(U0(q)).digest(), J = $(); return Q.outputLen = J.outputLen, Q.blockLen = J.blockLen, Q.create = () => $(), Q } class k0 { clone() { return this._cloneInto() } } var p8, a, n8, x$; var M0 = z(() => {/*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */p8 = ($) => $ instanceof Uint8Array, a = ($) => new DataView($.buffer, $.byteOffset, $.byteLength), n8 = new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68; if (!n8) throw new Error("Non little-endian hardware is not supported"); x$ = {}.toString }); class O0 extends k0 { constructor($, Q, J, q) { super(); this.blockLen = $, this.outputLen = Q, this.padOffset = J, this.isLE = q, this.finished = !1, this.length = 0, this.pos = 0, this.destroyed = !1, this.buffer = new Uint8Array($), this.view = a(this.buffer) } update($) { F0(this); const { view: Q, buffer: J, blockLen: q } = this; $ = U0($); const N = $.length; for (let X = 0; X < N;) { const Y = Math.min(q - this.pos, N - X); if (Y === q) { const Z = a($); for (; q <= N - X; X += q)this.process(Z, X); continue } if (J.set($.subarray(X, X + Y), this.pos), this.pos += Y, X += Y, this.pos === q) this.process(Q, 0), this.pos = 0 } return this.length += $.length, this.roundClean(), this } digestInto($) { F0(this), m0($, this), this.finished = !0; const { buffer: Q, view: J, blockLen: q, isLE: N } = this; let { pos: X } = this; if (Q[X++] = 128, this.buffer.subarray(X).fill(0), this.padOffset > q - X) this.process(J, 0), X = 0; for (let I = X; I < q; I++)Q[I] = 0; l8(J, q - 8, BigInt(this.length * 8), N), this.process(J, 0); const Y = a($), Z = this.outputLen; if (Z % 4) throw new Error("_sha2: outputLen should be aligned to 32bit"); const T = Z / 4, U = this.get(); if (T > U.length) throw new Error("_sha2: outputLen bigger than state"); for (let I = 0; I < T; I++)Y.setUint32(4 * I, U[I], N) } digest() { const { buffer: $, outputLen: Q } = this; this.digestInto($); const J = $.slice(0, Q); return this.destroy(), J } _cloneInto($) { $ || ($ = new this.constructor), $.set(...this.get()); const { blockLen: Q, buffer: J, length: q, finished: N, destroyed: X, pos: Y } = this; if ($.length = q, $.pos = Y, $.finished = N, $.destroyed = X, q % Q) $.buffer.set(J); return $ } } var l8; var i0 = z(() => { d0(); M0(); l8 = ($, Q, J, q) => { if (typeof $.setBigUint64 === "function") return $.setBigUint64(Q, J, q); const N = BigInt(32), X = BigInt(4294967295), Y = Number(J >> N & X), Z = Number(J & X), T = q ? 4 : 0, U = q ? 0 : 4; $.setUint32(Q + T, Y, q), $.setUint32(Q + U, Z, q) } }); c0(); function s($) { return $.message !== void 0 } var h8 = 1, r8 = 2; class t { t; n; key_label; metadata; constructor({ t: $, n: Q, key_label: J, permissions: q, ephPK: N }) { if (this.t = $, this.n = Q, this.key_label = J, this.metadata = [], q) this.metadata.push({ tag: h8, value: q }); if (N) this.metadata.push({ tag: r8, value: N }) } } class l0 { authModule; threshold; totalNodes; wp_client; constructor($, Q, J, q) { if (Q === 0) throw new Error("Threshold cannot be 0"); this.threshold = Q, this.totalNodes = J, this.authModule = q, this.wp_client = $ } async authenticateAndCreateKey($, Q) { try { const J = new t({ t: this.threshold, n: this.totalNodes, permissions: Q, ephPK: _($) }); const q = (X) => this.authModule.authenticate({ setup: J, challenge: X }), N = await this.wp_client.startKeygen({ setup: J, signer: q }); return undefined, N } catch (J) { throw console.error(J), J } } async authenticateAndSign($, Q) { const J = { t: this.threshold, key_id: $, message: Q }, q = (X) => this.authModule.authenticate({ setup: J, challenge: X }), N = await this.wp_client.startSigngen({ setup: J, signer: q }); return undefined, N } } var $$ = ($, Q) => { if ($) return z0(Q.hashable).then(Q.finish); return Q.finish(J8(Q.hashable)) };/*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) */var j = 2n ** 255n - 19n, x = 2n ** 252n + 27742317777372353535851937790883648493n, R0 = 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an, G0 = 0x6666666666666666666666666666666666666666666666666666666666666658n, e = { a: -1n, d: 37095705934669439343138083508754565189542113879843219016388785533085940283555n, p: j, n: x, h: 8, Gx: R0, Gy: G0 }, W = ($ = "") => { throw new Error($) }, t0 = ($) => typeof $ === "string", o8 = ($) => $ instanceof Uint8Array || $ != null && typeof $ === "object" && $.constructor.name === "Uint8Array", p = ($, Q) => !o8($) || typeof Q === "number" && Q > 0 && $.length !== Q ? W("Uint8Array of valid length expected") : $, v = ($) => new Uint8Array($), Q0 = ($, Q) => p(t0($) ? W0($) : v(p($)), Q), V = ($, Q = j) => { const J = $ % Q; return J >= 0n ? J : Q + J }, h0 = ($) => $ instanceof w ? $ : W("Point expected"); class w { constructor($, Q, J, q) { this.ex = $, this.ey = Q, this.ez = J, this.et = q } static fromAffine($) { return new w($.x, $.y, 1n, V($.x * $.y)) } static fromHex($, Q = !1) { const { d: J } = e; $ = Q0($, 32); const q = $.slice(), N = $[31]; q[31] = N & ~128; const X = $8(q); if (Q && !(0n <= X && X < 2n ** 256n)) W("bad y coord 1"); if (!Q && !(0n <= X && X < j)) W("bad y coord 2"); const Y = V(X * X), Z = V(Y - 1n), T = V(J * Y + 1n); let { isValid: U, value: I } = t8(Z, T); if (!U) W("bad y coordinate 3"); const D = (I & 1n) === 1n, k = (N & 128) !== 0; if (!Q && I === 0n && k) W("bad y coord 3"); if (k !== D) I = V(-I); return new w(I, X, 1n, V(I * X)) } get x() { return this.toAffine().x } get y() { return this.toAffine().y } equals($) { const { ex: Q, ey: J, ez: q } = this, { ex: N, ey: X, ez: Y } = h0($), Z = V(Q * Y), T = V(N * q), U = V(J * Y), I = V(X * q); return Z === T && U === I } is0() { return this.equals(d) } negate() { return new w(V(-this.ex), this.ey, this.ez, V(-this.et)) } double() { const { ex: $, ey: Q, ez: J } = this, { a: q } = e, N = V($ * $), X = V(Q * Q), Y = V(2n * V(J * J)), Z = V(q * N), T = $ + Q, U = V(V(T * T) - N - X), I = Z + X, D = I - Y, k = Z - X, R = V(U * D), G = V(I * k), K = V(U * k), C = V(D * I); return new w(R, G, C, K) } add($) { const { ex: Q, ey: J, ez: q, et: N } = this, { ex: X, ey: Y, ez: Z, et: T } = h0($), { a: U, d: I } = e, D = V(Q * X), k = V(J * Y), R = V(N * I * T), G = V(q * Z), K = V((Q + J) * (X + Y) - D - k), C = V(G - R), A = V(G + R), M = V(k - U * D), L = V(K * C), E = V(A * M), c = V(K * M), m = V(C * A); return new w(L, E, m, c) } mul($, Q = !0) { if ($ === 0n) return Q === !0 ? W("cannot multiply by 0") : d; if (!(typeof $ === "bigint" && 0n < $ && $ < x)) W("invalid scalar, must be < L"); if (!Q && this.is0() || $ === 1n) return this; if (this.equals(g)) return q$($).p; let J = d, q = g; for (let N = this; $ > 0n; N = N.double(), $ >>= 1n)if ($ & 1n) J = J.add(N); else if (Q) q = q.add(N); return J } multiply($) { return this.mul($) } clearCofactor() { return this.mul(BigInt(e.h), !1) } isSmallOrder() { return this.clearCofactor().is0() } isTorsionFree() { let $ = this.mul(x / 2n, !1).double(); if (x % 2n) $ = $.add(this); return $.is0() } toAffine() { const { ex: $, ey: Q, ez: J } = this; if (this.equals(d)) return { x: 0n, y: 1n }; const q = Q8(J); if (V(J * q) !== 1n) W("invalid inverse"); return { x: V($ * q), y: V(Q * q) } } toRawBytes() { const { x: $, y: Q } = this.toAffine(), J = e0(Q); return J[31] |= $ & 1n ? 128 : 0, J } toHex() { return j0(this.toRawBytes()) } } w.BASE = new w(R0, G0, 1n, V(R0 * G0)); w.ZERO = new w(0n, 1n, 1n, 0n); var { BASE: g, ZERO: d } = w, s0 = ($, Q) => $.toString(16).padStart(Q, "0"), j0 = ($) => Array.from($).map((Q) => s0(Q, 2)).join(""), W0 = ($) => { const Q = $.length; if (!t0($) || Q % 2) W("hex invalid 1"); const J = v(Q / 2); for (let q = 0; q < J.length; q++) { const N = q * 2, X = $.slice(N, N + 2), Y = Number.parseInt(X, 16); if (Number.isNaN(Y) || Y < 0) W("hex invalid 2"); J[q] = Y } return J }, e0 = ($) => W0(s0($, 64)).reverse(), $8 = ($) => BigInt("0x" + j0(v(p($)).reverse())), $0 = (...$) => { const Q = v($.reduce((q, N) => q + p(N).length, 0)); let J = 0; return $.forEach((q) => { Q.set(q, J), J += q.length }), Q }, Q8 = ($, Q = j) => { if ($ === 0n || Q <= 0n) W("no inverse n=" + $ + " mod=" + Q); let J = V($, Q), q = Q, N = 0n, X = 1n, Y = 1n, Z = 0n; while (J !== 0n) { const T = q / J, U = q % J, I = N - Y * T, D = X - Z * T; q = J, J = U, N = Y, X = Z, Y = I, Z = D } return q === 1n ? V(N, Q) : W("no inverse") }, H = ($, Q) => { let J = $; while (Q-- > 0n) J *= J, J %= j; return J }, a8 = ($) => { const J = $ * $ % j * $ % j, q = H(J, 2n) * J % j, N = H(q, 1n) * $ % j, X = H(N, 5n) * N % j, Y = H(X, 10n) * X % j, Z = H(Y, 20n) * Y % j, T = H(Z, 40n) * Z % j, U = H(T, 80n) * T % j, I = H(U, 80n) * T % j, D = H(I, 10n) * X % j; return { pow_p_5_8: H(D, 2n) * $ % j, b2: J } }, r0 = 19681161376707505956807079304988542015446066515923890162744021073123829784752n, t8 = ($, Q) => { const J = V(Q * Q * Q), q = V(J * J * Q), N = a8($ * q).pow_p_5_8; let X = V($ * J * N); const Y = V(Q * X * X), Z = X, T = V(X * r0), U = Y === $, I = Y === V(-$), D = Y === V(-$ * r0); if (U) X = Z; if (I || D) X = T; if ((V(X) & 1n) === 1n) X = V(-X); return { isValid: U || I, value: X } }, K0 = ($) => V($8($), x), u, z0 = (...$) => n.sha512Async(...$), J8 = (...$) => typeof u === "function" ? u(...$) : W("etc.sha512Sync not set"), q8 = ($) => { const Q = $.slice(0, 32); Q[0] &= 248, Q[31] &= 127, Q[31] |= 64; const J = $.slice(32, 64), q = K0(Q), N = g.mul(q), X = N.toRawBytes(); return { head: Q, prefix: J, scalar: q, point: N, pointBytes: X } }, s8 = ($) => z0(Q0($, 32)).then(q8), e8 = ($) => q8(J8(Q0($, 32))); var N8 = ($) => e8($).pointBytes, Q$ = ($, Q, J) => { const { pointBytes: q, scalar: N } = $, X = K0(Q), Y = g.mul(X).toRawBytes(); return { hashable: $0(Y, q, J), finish: (U) => { const I = V(X + K0(U) * N, x); return p($0(Y, e0(I)), 64) } } }, X8 = async ($, Q) => { const J = Q0($), q = await s8(Q), N = await z0(q.prefix, J); return $$(!0, Q$(q, N, J)) }; var o0 = () => typeof globalThis === "object" && ("crypto" in globalThis) ? globalThis.crypto : void 0, n = { bytesToHex: j0, hexToBytes: W0, concatBytes: $0, mod: V, invert: Q8, randomBytes: ($ = 32) => { const Q = o0(); if (!Q || !Q.getRandomValues) W("crypto.getRandomValues must be defined"); return Q.getRandomValues(v($)) }, sha512Async: async (...$) => { const Q = o0(); if (!Q || !Q.subtle) W("crypto.subtle or etc.sha512Async must be defined"); const J = $0(...$); return v(await Q.subtle.digest("SHA-512", J.buffer)) }, sha512Sync: void 0 }; Object.defineProperties(n, { sha512Sync: { configurable: !1, get() { return u }, set($) { if (!u) u = $ } } }); var b = 8, J$ = () => { const $ = [], Q = 256 / b + 1; let J = g, q = J; for (let N = 0; N < Q; N++) { q = J, $.push(q); for (let X = 1; X < 2 ** (b - 1); X++)q = q.add(J), $.push(q); J = q.double() } return $ }, a0 = void 0, q$ = ($) => { const Q = a0 || (a0 = J$()), J = (I, D) => { const k = D.negate(); return I ? k : D }; let q = d, N = g; const X = 1 + 256 / b, Y = 2 ** (b - 1), Z = BigInt(2 ** b - 1), T = 2 ** b, U = BigInt(b); for (let I = 0; I < X; I++) { const D = I * Y; let k = Number($ & Z); if ($ >>= U, k > Y) k -= T, $ += 1n; const R = D, G = D + Math.abs(k) - 1, K = I % 2 !== 0, C = k < 0; if (k === 0) N = N.add(J(K, Q[R])); else q = q.add(J(C, Q[G])) } return { p: q, f: N } }; i0(); p0(); M0(); var [N$, X$] = (() => F.split(["0x428a2f98d728ae22", "0x7137449123ef65cd", "0xb5c0fbcfec4d3b2f", "0xe9b5dba58189dbbc", "0x3956c25bf348b538", "0x59f111f1b605d019", "0x923f82a4af194f9b", "0xab1c5ed5da6d8118", "0xd807aa98a3030242", "0x12835b0145706fbe", "0x243185be4ee4b28c", "0x550c7dc3d5ffb4e2", "0x72be5d74f27b896f", "0x80deb1fe3b1696b1", "0x9bdc06a725c71235", "0xc19bf174cf692694", "0xe49b69c19ef14ad2", "0xefbe4786384f25e3", "0x0fc19dc68b8cd5b5", "0x240ca1cc77ac9c65", "0x2de92c6f592b0275", "0x4a7484aa6ea6e483", "0x5cb0a9dcbd41fbd4", "0x76f988da831153b5", "0x983e5152ee66dfab", "0xa831c66d2db43210", "0xb00327c898fb213f", "0xbf597fc7beef0ee4", "0xc6e00bf33da88fc2", "0xd5a79147930aa725", "0x06ca6351e003826f", "0x142929670a0e6e70", "0x27b70a8546d22ffc", "0x2e1b21385c26c926", "0x4d2c6dfc5ac42aed", "0x53380d139d95b3df", "0x650a73548baf63de", "0x766a0abb3c77b2a8", "0x81c2c92e47edaee6", "0x92722c851482353b", "0xa2bfe8a14cf10364", "0xa81a664bbc423001", "0xc24b8b70d0f89791", "0xc76c51a30654be30", "0xd192e819d6ef5218", "0xd69906245565a910", "0xf40e35855771202a", "0x106aa07032bbd1b8", "0x19a4c116b8d2d0c8", "0x1e376c085141ab53", "0x2748774cdf8eeb99", "0x34b0bcb5e19b48a8", "0x391c0cb3c5c95a63", "0x4ed8aa4ae3418acb", "0x5b9cca4f7763e373", "0x682e6ff3d6b2b8a3", "0x748f82ee5defb2fc", "0x78a5636f43172f60", "0x84c87814a1f0ab72", "0x8cc702081a6439ec", "0x90befffa23631e28", "0xa4506cebde82bde9", "0xbef9a3f7b2c67915", "0xc67178f2e372532b", "0xca273eceea26619c", "0xd186b8c721c0c207", "0xeada7dd6cde0eb1e", "0xf57d4f7fee6ed178", "0x06f067aa72176fba", "0x0a637dc5a2c898a6", "0x113f9804bef90dae", "0x1b710b35131c471b", "0x28db77f523047d84", "0x32caab7b40c72493", "0x3c9ebe0a15c9bebc", "0x431d67c49c100d4c", "0x4cc5d4becb3e42b6", "0x597f299cfc657e2a", "0x5fcb6fab3ad6faec", "0x6c44198c4a475817"].map(($) => BigInt($))))(), S = new Uint32Array(80), y = new Uint32Array(80); class Z8 extends O0 { constructor() { super(128, 64, 16, !1); this.Ah = 1779033703 | 0, this.Al = 4089235720 | 0, this.Bh = 3144134277 | 0, this.Bl = 2227873595 | 0, this.Ch = 1013904242 | 0, this.Cl = 4271175723 | 0, this.Dh = 2773480762 | 0, this.Dl = 1595750129 | 0, this.Eh = 1359893119 | 0, this.El = 2917565137 | 0, this.Fh = 2600822924 | 0, this.Fl = 725511199 | 0, this.Gh = 528734635 | 0, this.Gl = 4215389547 | 0, this.Hh = 1541459225 | 0, this.Hl = 327033209 | 0 } get() { const { Ah: $, Al: Q, Bh: J, Bl: q, Ch: N, Cl: X, Dh: Y, Dl: Z, Eh: T, El: U, Fh: I, Fl: D, Gh: k, Gl: R, Hh: G, Hl: K } = this; return [$, Q, J, q, N, X, Y, Z, T, U, I, D, k, R, G, K] } set($, Q, J, q, N, X, Y, Z, T, U, I, D, k, R, G, K) { this.Ah = $ | 0, this.Al = Q | 0, this.Bh = J | 0, this.Bl = q | 0, this.Ch = N | 0, this.Cl = X | 0, this.Dh = Y | 0, this.Dl = Z | 0, this.Eh = T | 0, this.El = U | 0, this.Fh = I | 0, this.Fl = D | 0, this.Gh = k | 0, this.Gl = R | 0, this.Hh = G | 0, this.Hl = K | 0 } process($, Q) { for (let M = 0; M < 16; M++, Q += 4)S[M] = $.getUint32(Q), y[M] = $.getUint32(Q += 4); for (let M = 16; M < 80; M++) { const L = S[M - 15] | 0, E = y[M - 15] | 0, c = F.rotrSH(L, E, 1) ^ F.rotrSH(L, E, 8) ^ F.shrSH(L, E, 7), m = F.rotrSL(L, E, 1) ^ F.rotrSL(L, E, 8) ^ F.shrSL(L, E, 7), f = S[M - 2] | 0, B = y[M - 2] | 0, i = F.rotrSH(f, B, 19) ^ F.rotrBH(f, B, 61) ^ F.shrSH(f, B, 6), J0 = F.rotrSL(f, B, 19) ^ F.rotrBL(f, B, 61) ^ F.shrSL(f, B, 6), l = F.add4L(m, J0, y[M - 7], y[M - 16]), q0 = F.add4H(l, c, i, S[M - 7], S[M - 16]); S[M] = q0 | 0, y[M] = l | 0 } let { Ah: J, Al: q, Bh: N, Bl: X, Ch: Y, Cl: Z, Dh: T, Dl: U, Eh: I, El: D, Fh: k, Fl: R, Gh: G, Gl: K, Hh: C, Hl: A } = this; for (let M = 0; M < 80; M++) { const L = F.rotrSH(I, D, 14) ^ F.rotrSH(I, D, 18) ^ F.rotrBH(I, D, 41), E = F.rotrSL(I, D, 14) ^ F.rotrSL(I, D, 18) ^ F.rotrBL(I, D, 41), c = I & k ^ ~I & G, m = D & R ^ ~D & K, f = F.add5L(A, E, m, X$[M], y[M]), B = F.add5H(f, C, L, c, N$[M], S[M]), i = f | 0, J0 = F.rotrSH(J, q, 28) ^ F.rotrBH(J, q, 34) ^ F.rotrBH(J, q, 39), l = F.rotrSL(J, q, 28) ^ F.rotrBL(J, q, 34) ^ F.rotrBL(J, q, 39), q0 = J & N ^ J & Y ^ N & Y, M8 = q & X ^ q & Z ^ X & Z; C = G | 0, A = K | 0, G = k | 0, K = R | 0, k = I | 0, R = D | 0, { h: I, l: D } = F.add(T | 0, U | 0, B | 0, i | 0), T = Y | 0, U = Z | 0, Y = N | 0, Z = X | 0, N = J | 0, X = q | 0; const w0 = F.add3L(i, l, M8); J = F.add3H(w0, B, J0, q0), q = w0 | 0 } ({ h: J, l: q } = F.add(this.Ah | 0, this.Al | 0, J | 0, q | 0)), { h: N, l: X } = F.add(this.Bh | 0, this.Bl | 0, N | 0, X | 0), { h: Y, l: Z } = F.add(this.Ch | 0, this.Cl | 0, Y | 0, Z | 0), { h: T, l: U } = F.add(this.Dh | 0, this.Dl | 0, T | 0, U | 0), { h: I, l: D } = F.add(this.Eh | 0, this.El | 0, I | 0, D | 0), { h: k, l: R } = F.add(this.Fh | 0, this.Fl | 0, k | 0, R | 0), { h: G, l: K } = F.add(this.Gh | 0, this.Gl | 0, G | 0, K | 0), { h: C, l: A } = F.add(this.Hh | 0, this.Hl | 0, C | 0, A | 0), this.set(J, q, N, X, Y, Z, T, U, I, D, k, R, G, K, C, A) } roundClean() { S.fill(0), y.fill(0) } destroy() { this.buffer.fill(0), this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) } } var I8 = n0(() => new Z8); var T$ = ($, Q, J) => { const q = new t({ t: $.t, n: $.n, key_label: $.key_label, permissions: void 0, ephPK: _(J) }); return undefined, { types: { EIP712Domain: V$, ...Z$ }, domain: I$, primaryType: "Request", message: { setup: q, challenge: Q } } }; async function V8({ setup: $, user_id: Q, challenge: J, browserWallet: q, ephPK: N, lifetime: X }) { if (s($)) throw new Error("EOA auth cannot be used for Sign requests, please use EphAuth instead"); const Y = T$($, J, N); const Z = await q.signTypedData(Q, Y); const T = { eoa: Q, ephPK: _(N), expiry: Math.floor(Date.now() / 1000) + X }; return { credentials: { credentials: JSON.stringify(T), method: "eoa", id: Q }, signature: Z } } async function T8({ setup: $, user_id: Q, challenge: J, ephSK: q, ephPK: N }) { const X = { setup: $, challenge: J }, Z = _(await X8((new TextEncoder()).encode(JSON.stringify(X)), q)); const T = { eoa: Q, ephPK: _(N), expiry: 0 }; return { credentials: { credentials: JSON.stringify(T), method: "ephemeral", id: Q }, signature: Z } } n.sha512Sync = (...$) => I8(n.concatBytes(...$)); var Y$ = [{ name: "tag", type: "uint16" }, { name: "value", type: "string" }], Z$ = { Request: [{ name: "setup", type: "KeygenSetupOpts" }, { name: "challenge", type: "string" }], KeygenSetupOpts: [{ name: "t", type: "uint32" }, { name: "n", type: "uint32" }, { name: "metadata", type: "TaggedValue[]" }], TaggedValue: Y$ }, I$ = { name: "SilentShard authentication", version: "0.1.0" }, V$ = [{ name: "name", type: "string" }, { name: "version", type: "string" }]; var C0; ((J) => { J[J["EOA"] = 0] = "EOA"; J[J["NONE"] = 1] = "NONE" })(C0 || (C0 = {})); class F8 { userId; browserWallet; ephPK; lifetime; constructor($, Q, J, q = 3600) { this.userId = $, this.browserWallet = Q, this.ephPK = J, this.lifetime = q } async authenticate({ setup: $, challenge: Q }) { return await V8({ setup: $, user_id: this.userId, challenge: Q, browserWallet: this.browserWallet, ephPK: this.ephPK, lifetime: this.lifetime }) } } class D8 { userId; ephSK; ephPK; constructor($, Q) { this.userId = $, this.ephSK = Q, this.ephPK = N8(this.ephSK) } async authenticate({ setup: $, challenge: Q }) { return await T8({ setup: $, user_id: this.userId, challenge: Q, ephSK: this.ephSK, ephPK: this.ephPK }) } } var U8 = ($) => btoa(String.fromCodePoint.apply(null, Array.from($))); var O; ((N) => { N[N["initiated"] = 0] = "initiated"; N[N["waitingForSign"] = 1] = "waitingForSign"; N[N["waitingForResult"] = 2] = "waitingForResult"; N[N["finished"] = 3] = "finished" })(O || (O = {})); class k8 { walletProviderId; walletProviderUrl; constructor($) { this.walletProviderId = $.walletProviderId, this.walletProviderUrl = $.walletProviderUrl } getWalletId() { return this.walletProviderId } async startKeygen({ setup: $, signer: Q }) { return this.connect($, Q).then((J) => { const q = J.split(":"), N = q[1].split("=")[1], X = q[0].split("=")[1]; return { publicKey: N, keyId: X } }) } async startSigngen({ setup: $, signer: Q }) { return this.connect($, Q).then((J) => { const q = J.split(":"), N = q[0].split("=")[1], X = q[1].split("=")[1]; return { sign: N, recid: Number.parseInt(X) } }) } connect($, Q) { return new Promise((J, q) => { let N = O.initiated, X; if (s($)) X = "signgen", $.message = U8((new TextEncoder()).encode($.message)); else X = "keygen"; const Y = new WebSocket(`${this.walletProviderUrl}/${X}`); Y.addEventListener("open", (Z) => { switch (console.log(`Connection opened in state ${N} with event ${JSON.stringify(Z, void 0, "\t")}`), N) { case O.initiated: N = O.waitingForSign, Y.send(JSON.stringify($)); break; case O.waitingForSign: case O.waitingForResult: N = O.finished, q("Incorrect protocol state"); break; case O.finished: break } }), Y.addEventListener("message", async (Z) => { switch (console.log(`Connection message in state ${N} with event ${JSON.stringify(Z, void 0, "\t")}`), N) { case O.initiated: N = O.finished, q("Incorrect protocol state"); break; case O.waitingForSign: { N = O.waitingForResult; const T = await Q(Z.data); Y.send(JSON.stringify(T)); break } case O.waitingForResult: N = O.finished, Y.close(), J(Z.data); break; case O.finished: break } }), Y.addEventListener("error", (Z) => { if (console.log(`Connection error in state ${N} with event ${JSON.stringify(Z, void 0, "\t")}`), N != O.finished) N = O.finished, q("Incorrect protocol state") }), Y.addEventListener("close", (Z) => { if (console.log(`Connection closed in state ${N} with event ${JSON.stringify(Z, void 0, "\t")}`), N != O.finished) N = O.finished, q("Incorrect protocol state") }) }) } } export { k8 as WalletProviderServiceClient, l0 as NetworkSigner, D8 as EphAuth, F8 as EOAAuth, C0 as AuthMethod }; diff --git a/src/modules/walletprovider-sdk/networkSigner.ts b/src/modules/walletprovider-sdk/networkSigner.ts new file mode 100644 index 000000000..859cb2079 --- /dev/null +++ b/src/modules/walletprovider-sdk/networkSigner.ts @@ -0,0 +1,99 @@ +import type { AuthModule, UserAuthentication } from './authentication'; +import type { IWalletProviderServiceClient } from './walletProviderServiceClientInterface'; +/** + * Response from the network for keygen requests + * @alpha + */ +export interface KeygenResponse { + keyId: string; + publicKey: string; +} +/** + * Response from the network for sign request + * @alpha + */ +export interface SignResponse { + sign: string; + recid: number; +} +/** Key parameters used during generation */ +export declare class KeygenSetupOpts { + /** Threshold, number of parties that needs to participate in a protocol in order to produce valid signature */ + t: number; + /** Total number of nodes that participate in Key generation, must be greater or equal than `t` */ + n: number; + /** Optional key label */ + key_label?: string; + /** Metadata for a key. Currently they store the permissions, can be set in a constructor of this class. + If permissions are not set, all operations are allowed. + */ + metadata: { + tag: number; + value: string; + }[]; + constructor({ t, n, key_label, permissions, ephPK, }: { + t: number; + n: number; + key_label?: string; + permissions?: string; + ephPK?: string; + }); +} +/** Parameters used in Signature execution */ +export type SignSetupOpts = { + /** Nymber of nodes that will participate in the signature execution */ + t: number; + /** Select the key using it's ID */ + key_id: string; + /** The message to sign */ + message: string; +}; +/** Verifies if passed `params` object is of type SignSetupOpts */ +export declare function isSignSetupOpts(params: KeygenSetupOpts | SignSetupOpts): params is SignSetupOpts; +/** The `user_authentication` contains signature over the `setup` parameter. */ +export type UserAuthenticatedRequest = { + setup: T; + user_authentication: UserAuthentication; +}; +/** The networkSigner contains an API to communicate with the Silent MPC Network. Call to sign and keygen require + * the Auth module, that is used to prompt the User before executing the request. + * @alpha + */ +export declare class NetworkSigner { + /** Authentication module, used to get confirmation from the User before request execution */ + authModule: AuthModule; + /** Number of nodes that needs to participate in protocol in order to generate valid signature. Also known as `t`. */ + threshold: number; + /** Number of nodes that participate in keygen operation. Also known as `n`. */ + totalNodes: number; + /** Wallet Provider backend client */ + wp_client: IWalletProviderServiceClient; + /** + * Facade class used to execute operations on Silent Network. + * @param wpClient - Wallet Provider backend client + * @param threshold - Number of nodes that needs to participate in protocol in order to generate valid signature. Also known as `t`. + * @param totalNodes - Number of nodes that participate in keygen operation. Also known as `n`. + * @param authModule - Authentication module, used to get confirmation from the User before request execution + */ + constructor(wpClient: IWalletProviderServiceClient, threshold: number, totalNodes: number, authModule: AuthModule); + /** API to generate a distributed key that's generated by Silent Network. + * Uses `authModule` to get authentication of the request from the User + * @param ephKey - ephemeral key used to authenticate the user during the session. + * @param permissions - optional permissions that will be stored in the key metadata. + * The permissions are validated during sign requests. + * @returns {@link KeygenResponse} containing `keyId` and the `pubKey` public part of the key + * @public + * @alpha + */ + authenticateAndCreateKey(ephKey: Uint8Array, permissions?: string): Promise; + /** Generate a signature. Uses `authModule` to authenticate the sign request by the User. + * The network chooses `t` nodes to execute the protocol. + * @param keyId - the key id returned from `authenticateAndCreateKey` + * @param message - the message to sign by the MPC network + * @returns {@link SignResponse} + * @public + * @alpha + */ + authenticateAndSign(keyId: string, message: string): Promise; +} +//# sourceMappingURL=networkSigner.d.ts.map \ No newline at end of file diff --git a/src/modules/walletprovider-sdk/types.ts b/src/modules/walletprovider-sdk/types.ts new file mode 100644 index 000000000..2520139b8 --- /dev/null +++ b/src/modules/walletprovider-sdk/types.ts @@ -0,0 +1,5 @@ +export type { NetworkSigner, SignResponse, KeygenResponse } from './networkSigner'; +export type { AuthMethod, EOAAuth, EphAuth } from './authentication'; +export type { IBrowserWallet, TypedData } from './EOAauthentication'; +export type { ClientConfig, IWalletProviderServiceClient } from './walletProviderServiceClientInterface'; +export type { WalletProviderServiceClient } from './walletProviderServiceClient'; \ No newline at end of file diff --git a/src/modules/walletprovider-sdk/viemSigner.ts b/src/modules/walletprovider-sdk/viemSigner.ts new file mode 100644 index 000000000..f1d194345 --- /dev/null +++ b/src/modules/walletprovider-sdk/viemSigner.ts @@ -0,0 +1,12 @@ +import type * as viem from 'viem'; +import type { NetworkSigner } from './networkSigner'; +/** + * Create a new viem custom account for signing transactions using + * the MPC network. + * @param clusterConfig The network cluster configuration. + * @param keyId The selected Key ID. + * @param publicKey Associated public key of the selected Key ID. + * @param threshold The threshold. + */ +export declare function createViemAccount(networkSigner: NetworkSigner, keyId: string, publicKey: string): viem.LocalAccount; +//# sourceMappingURL=viemSigner.d.ts.map \ No newline at end of file diff --git a/src/modules/walletprovider-sdk/walletProviderServiceClient.ts b/src/modules/walletprovider-sdk/walletProviderServiceClient.ts new file mode 100644 index 000000000..30dd71b47 --- /dev/null +++ b/src/modules/walletprovider-sdk/walletProviderServiceClient.ts @@ -0,0 +1,27 @@ +import type { KeygenResponse, KeygenSetupOpts, SignResponse, SignSetupOpts } from './networkSigner'; +import type { ClientConfig, IWalletProviderServiceClient, Signer } from './walletProviderServiceClientInterface'; +/** + * The Websocket client to the Wallet Provider backend service. + * All requests are relayed by this entity to the MPC network. + * @alpha + */ +export declare class WalletProviderServiceClient implements IWalletProviderServiceClient { + walletProviderId: string; + walletProviderUrl: string; + /** + * Create new client that connects to the backend service + * @param config - config containing information about backend service + */ + constructor(config: ClientConfig); + getWalletId(): string; + startKeygen({ setup, signer }: { + setup: KeygenSetupOpts; + signer: Signer; + }): Promise; + startSigngen({ setup, signer }: { + setup: SignSetupOpts; + signer: Signer; + }): Promise; + connect(params: KeygenSetupOpts | SignSetupOpts, signer: Signer): Promise; +} +//# sourceMappingURL=walletProviderServiceClient.d.ts.map \ No newline at end of file diff --git a/src/modules/walletprovider-sdk/walletProviderServiceClientInterface.ts b/src/modules/walletprovider-sdk/walletProviderServiceClientInterface.ts new file mode 100644 index 000000000..530c6ef76 --- /dev/null +++ b/src/modules/walletprovider-sdk/walletProviderServiceClientInterface.ts @@ -0,0 +1,35 @@ +import type { UserAuthentication } from './authentication'; +import type { KeygenResponse, KeygenSetupOpts, SignResponse, SignSetupOpts } from './networkSigner'; +/** + * The config used to create Wallet Provider Service backend client. + * implementation for requirements that the backend service must fulfill. + * @alpha + */ +export type ClientConfig = { + /** + * The id of the Wallet Provider Service + * @alpha + */ + walletProviderId: string; + /** + * The URL used to connect to the service + * @alpha + */ + walletProviderUrl: string; +}; +export type Signer = (challenge: string) => Promise; +/** Interface for client of Wallet Provider Service + * @alpha + */ +export interface IWalletProviderServiceClient { + getWalletId(): string; + startKeygen({ setup, signer }: { + setup: KeygenSetupOpts; + signer: Signer; + }): Promise; + startSigngen({ setup, signer }: { + setup: SignSetupOpts; + signer: Signer; + }): Promise; +} +//# sourceMappingURL=walletProviderServiceClientInterface.d.ts.map \ No newline at end of file diff --git a/tests/globalSetup.ts b/tests/globalSetup.ts index 302b13901..4de96ad79 100644 --- a/tests/globalSetup.ts +++ b/tests/globalSetup.ts @@ -2,6 +2,7 @@ import { config } from "dotenv" import { getConfig } from "./utils.js" config() + export default function setup({ provide: _ }) { getConfig() } diff --git a/tests/modules/write.test.ts b/tests/modules/write.test.ts index 882644a7c..9650197b2 100644 --- a/tests/modules/write.test.ts +++ b/tests/modules/write.test.ts @@ -10,19 +10,19 @@ import { parseEther, parseUnits, slice, - toFunctionSelector -} from "viem" -import { privateKeyToAccount } from "viem/accounts" -import { beforeAll, describe, expect, test } from "vitest" + toFunctionSelector, +} from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { beforeAll, describe, expect, test } from "vitest"; import { type BiconomySmartAccountV2, type Transaction, type TransferOwnershipCompatibleModule, type WalletClientSigner, addressEquals, - createSmartAccountClient -} from "../../src/account" -import { Logger, getChain } from "../../src/account" + createSmartAccountClient, +} from "../../src/account"; +import { Logger, getChain } from "../../src/account"; import { type CreateSessionDataParams, DEFAULT_BATCHED_SESSION_ROUTER_MODULE, @@ -30,49 +30,52 @@ import { DEFAULT_MULTICHAIN_MODULE, DEFAULT_SESSION_KEY_MANAGER_MODULE, ECDSA_OWNERSHIP_MODULE_ADDRESSES_BY_VERSION, + type PolicyWithoutSessionKey, + createDistributedSession, createMultiChainValidationModule, createSessionKeyManagerModule, getABISVMSessionKeyData, - resumeSession -} from "../../src/modules" + resumeSession, +} from "../../src/modules"; -import { ECDSAModuleAbi } from "../../src/account/abi/ECDSAModule" -import { SessionMemoryStorage } from "../../src/modules/session-storage/SessionMemoryStorage" -import { createSessionKeyEOA } from "../../src/modules/session-storage/utils" +import { ECDSAModuleAbi } from "../../src/account/abi/ECDSAModule"; +import { SessionMemoryStorage } from "../../src/modules/session-storage/SessionMemoryStorage"; +import { createSessionKeyEOA } from "../../src/modules/session-storage/utils"; import { type Policy, + PolicyHelpers, createABISessionDatum, createSession, - getSingleSessionTxParams -} from "../../src/modules/sessions/abi" + getSingleSessionTxParams, +} from "../../src/modules/sessions/abi"; import { createBatchSession, - getBatchSessionTxParams -} from "../../src/modules/sessions/batch" -import { createERC20SessionDatum } from "../../src/modules/sessions/erc20" -import { createSessionSmartAccountClient } from "../../src/modules/sessions/sessionSmartAccountClient" -import { PaymasterMode } from "../../src/paymaster" + getBatchSessionTxParams, +} from "../../src/modules/sessions/batch"; +import { createERC20SessionDatum } from "../../src/modules/sessions/erc20"; +import { createSessionSmartAccountClient } from "../../src/modules/sessions/sessionSmartAccountClient"; +import { PaymasterMode } from "../../src/paymaster"; import { checkBalance, getBundlerUrl, getConfig, nonZeroBalance, - topUp -} from "../utils" + topUp, +} from "../utils"; describe("Modules:Write", () => { - const nonceOptions = { nonceKey: Date.now() + 30 } - const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e" - const token = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a" - const preferredToken = token - const BICONOMY_TOKEN_PAYMASTER = "0x00000f7365cA6C59A2C93719ad53d567ed49c14C" - const amount = parseUnits(".0001", 6) + const nonceOptions = { nonceKey: Date.now() + 30 }; + const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e"; + const token = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a"; + const preferredToken = token; + const BICONOMY_TOKEN_PAYMASTER = "0x00000f7365cA6C59A2C93719ad53d567ed49c14C"; + const amount = parseUnits(".0001", 6); const DUMMY_CONTRACT_ADDRESS: Hex = - "0xA975e69917A4c856b17Fc8Cc4C352f326Ef21C6B" // amoy address + "0xA975e69917A4c856b17Fc8Cc4C352f326Ef21C6B"; // amoy address const withSponsorship = { - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - } + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + }; const { chain, @@ -81,76 +84,76 @@ describe("Modules:Write", () => { privateKeyTwo, bundlerUrl, paymasterUrl, - paymasterUrlTwo - } = getConfig() - const account = privateKeyToAccount(`0x${privateKey}`) - const accountTwo = privateKeyToAccount(`0x${privateKeyTwo}`) + paymasterUrlTwo, + } = getConfig(); + const account = privateKeyToAccount(`0x${privateKey}`); + const accountTwo = privateKeyToAccount(`0x${privateKeyTwo}`); const publicClient = createPublicClient({ chain, - transport: http() - }) + transport: http(), + }); let [ smartAccount, smartAccountTwo, smartAccountThree, - smartAccountFour - ]: BiconomySmartAccountV2[] = [] + smartAccountFour, + ]: BiconomySmartAccountV2[] = []; let [ smartAccountAddress, smartAccountAddressTwo, smartAccountAddressThree, - smartAccountAddressFour - ]: Hex[] = [] + smartAccountAddressFour, + ]: Hex[] = []; const [walletClient, walletClientTwo] = [ createWalletClient({ account, chain, - transport: http() + transport: http(), }), createWalletClient({ account: accountTwo, chain, - transport: http() - }) - ] + transport: http(), + }), + ]; - const recipient = walletClientTwo.account.address + const recipient = walletClientTwo.account.address; beforeAll(async () => { - ;[smartAccount, smartAccountTwo] = await Promise.all( + [smartAccount, smartAccountTwo] = await Promise.all( [walletClient, walletClientTwo].map((client) => createSmartAccountClient({ chainId, signer: client, bundlerUrl, - paymasterUrl - }) - ) - ) - ;[smartAccountAddress, smartAccountAddressTwo] = await Promise.all( + paymasterUrl, + }), + ), + ); + [smartAccountAddress, smartAccountAddressTwo] = await Promise.all( [smartAccount, smartAccountTwo].map((account) => - account.getAccountAddress() - ) - ) + account.getAccountAddress(), + ), + ); smartAccountThree = await createSmartAccountClient({ signer: walletClient, bundlerUrl, paymasterUrl, - index: 7 - }) + index: 7, + }); smartAccountFour = await createSmartAccountClient({ signer: walletClient, bundlerUrl, paymasterUrl, - index: 6 - }) + index: 6, + }); - smartAccountAddressThree = await smartAccountThree.getAccountAddress() - smartAccountAddressFour = await smartAccountFour.getAccountAddress() + smartAccountAddressThree = await smartAccountThree.getAccountAddress(); + smartAccountAddressFour = await smartAccountFour.getAccountAddress(); await Promise.all([ topUp(smartAccountAddress, undefined, token), @@ -160,14 +163,14 @@ describe("Modules:Write", () => { topUp(smartAccountAddressThree, undefined, token), topUp(smartAccountAddressThree, undefined), topUp(smartAccountAddressFour, undefined, token), - topUp(smartAccountAddressFour, undefined) - ]) - }) + topUp(smartAccountAddressFour, undefined), + ]); + }); // User must be connected with a wallet to grant permissions test("should create a single session on behalf of a user", async () => { const { sessionKeyAddress, sessionStorageClient } = - await createSessionKeyEOA(smartAccountThree, chain) + await createSessionKeyEOA(smartAccountThree, chain); const { wait, session } = await createSession( smartAccountThree, @@ -180,28 +183,28 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, - referenceValue: smartAccountAddressThree - } + referenceValue: smartAccountAddressThree, + }, ], interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, - valueLimit: 0n - } + valueLimit: 0n, + }, ], sessionStorageClient, - withSponsorship - ) + withSponsorship, + ); const { receipt: { transactionHash }, - success - } = await wait() + success, + } = await wait(); - expect(success).toBe("true") - Logger.log("Tx Hash: ", transactionHash) - }, 50000) + expect(success).toBe("true"); + Logger.log("Tx Hash: ", transactionHash); + }, 50000); // User no longer has to be connected, // Only the reference to the relevant sessionID and the store from the previous step is needed to execute txs on the user's behalf @@ -212,57 +215,57 @@ describe("Modules:Write", () => { accountAddress: smartAccountAddressThree, // Set the account address on behalf of the user bundlerUrl, paymasterUrl, - chainId + chainId, }, - smartAccountAddressThree // Storage client, full Session or smartAccount address if using default storage - ) + smartAccountAddressThree, // Storage client, full Session or smartAccount address if using default storage + ); const sessionSmartAccountThreeAddress = - await smartAccountThreeWithSession.getAccountAddress() + await smartAccountThreeWithSession.getAccountAddress(); - expect(sessionSmartAccountThreeAddress).toEqual(smartAccountAddressThree) + expect(sessionSmartAccountThreeAddress).toEqual(smartAccountAddressThree); const nftMintTx = { to: nftAddress, data: encodeFunctionData({ abi: parseAbi(["function safeMint(address _to)"]), functionName: "safeMint", - args: [smartAccountAddressThree] - }) - } + args: [smartAccountAddressThree], + }), + }; const nftBalanceBefore = await checkBalance( smartAccountAddressThree, - nftAddress - ) + nftAddress, + ); const { wait } = await smartAccountThreeWithSession.sendTransaction( nftMintTx, - { ...withSponsorship, nonceOptions } - ) + { ...withSponsorship, nonceOptions }, + ); - const { success } = await wait() + const { success } = await wait(); - expect(success).toBe("true") + expect(success).toBe("true"); const nftBalanceAfter = await checkBalance( smartAccountAddressThree, - nftAddress - ) + nftAddress, + ); - expect(nftBalanceAfter - nftBalanceBefore).toBe(1n) - }) + expect(nftBalanceAfter - nftBalanceBefore).toBe(1n); + }); // User must be connected with a wallet to grant permissions test("should create a batch session on behalf of a user", async () => { const { sessionKeyAddress, sessionStorageClient } = - await createSessionKeyEOA(smartAccountFour, chain) + await createSessionKeyEOA(smartAccountFour, chain); const leaves: CreateSessionDataParams[] = [ createERC20SessionDatum({ interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, sessionKeyData: encodeAbiParameters( @@ -270,15 +273,15 @@ describe("Modules:Write", () => { { type: "address" }, { type: "address" }, { type: "address" }, - { type: "uint256" } + { type: "uint256" }, ], - [sessionKeyAddress, token, recipient, amount] - ) + [sessionKeyAddress, token, recipient, amount], + ), }), createABISessionDatum({ interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: nftAddress, @@ -287,37 +290,37 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, - referenceValue: smartAccountAddressFour - } + referenceValue: smartAccountAddressFour, + }, ], - valueLimit: 0n - }) - ] + valueLimit: 0n, + }), + ]; const { wait, session } = await createBatchSession( smartAccountFour, sessionStorageClient, leaves, - withSponsorship - ) + withSponsorship, + ); const { receipt: { transactionHash }, - success - } = await wait() + success, + } = await wait(); - expect(success).toBe("true") + expect(success).toBe("true"); - Logger.log("Tx Hash: ", transactionHash) - Logger.log("session: ", { session }) - }, 50000) + Logger.log("Tx Hash: ", transactionHash); + Logger.log("session: ", { session }); + }, 50000); // User no longer has to be connected, // Only the reference to the relevant sessionID and the store from the previous step is needed to execute txs on the user's behalf test("should use the batch session to mint an NFT, and pay some token for the user", async () => { const { sessionStorageClient } = await resumeSession( - smartAccountAddressFour // Use the store from the previous test, you could pass in the smartAccount address, the full session or your custom sessionStorageClient - ) + smartAccountAddressFour, // Use the store from the previous test, you could pass in the smartAccount address, the full session or your custom sessionStorageClient + ); // Assume the real signer for userSmartAccountFour is no longer available (ie. user has logged out); const smartAccountFourWithSession = await createSessionSmartAccountClient( @@ -325,86 +328,86 @@ describe("Modules:Write", () => { accountAddress: sessionStorageClient.smartAccountAddress, // Set the account address on behalf of the user bundlerUrl, paymasterUrl, - chainId + chainId, }, smartAccountAddressFour, // Storage client, full Session or smartAccount address if using default storage - true // if batching - ) + true, // if batching + ); const sessionSmartAccountFourAddress = - await smartAccountFourWithSession.getAccountAddress() + await smartAccountFourWithSession.getAccountAddress(); expect( - addressEquals(sessionSmartAccountFourAddress, smartAccountAddressFour) - ).toBe(true) + addressEquals(sessionSmartAccountFourAddress, smartAccountAddressFour), + ).toBe(true); const transferTx: Transaction = { to: token, data: encodeFunctionData({ abi: parseAbi(["function transfer(address _to, uint256 _value)"]), functionName: "transfer", - args: [recipient, amount] - }) - } + args: [recipient, amount], + }), + }; const nftMintTx: Transaction = { to: nftAddress, data: encodeFunctionData({ abi: parseAbi(["function safeMint(address _to)"]), functionName: "safeMint", - args: [smartAccountAddressFour] - }) - } + args: [smartAccountAddressFour], + }), + }; const nftBalanceBefore = await checkBalance( smartAccountAddressFour, - nftAddress - ) - const tokenBalanceBefore = await checkBalance(recipient, token) + nftAddress, + ); + const tokenBalanceBefore = await checkBalance(recipient, token); - const txs = [transferTx, nftMintTx] + const txs = [transferTx, nftMintTx]; const batchSessionParams = await getBatchSessionTxParams( txs, [0, 1], smartAccountAddressFour, - chain - ) + chain, + ); const { wait } = await smartAccountFourWithSession.sendTransaction(txs, { ...batchSessionParams, ...withSponsorship, - nonceOptions - }) - const { success } = await wait() - expect(success).toBe("true") + nonceOptions, + }); + const { success } = await wait(); + expect(success).toBe("true"); - const tokenBalanceAfter = await checkBalance(recipient, token) + const tokenBalanceAfter = await checkBalance(recipient, token); const nftBalanceAfter = await checkBalance( smartAccountAddressFour, - nftAddress - ) - expect(tokenBalanceAfter - tokenBalanceBefore).toBe(amount) - expect(nftBalanceAfter - nftBalanceBefore).toBe(1n) - }, 50000) + nftAddress, + ); + expect(tokenBalanceAfter - tokenBalanceBefore).toBe(amount); + expect(nftBalanceAfter - nftBalanceBefore).toBe(1n); + }, 50000); test("should use MultichainValidationModule to mint an NFT on two chains with sponsorship", async () => { - const nftAddress: Hex = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e" + const nftAddress: Hex = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e"; - const chainIdBase = 84532 - const bundlerUrlBase = getBundlerUrl(chainIdBase) + const chainIdBase = 84532; + const bundlerUrlBase = getBundlerUrl(chainIdBase); const signerBase = createWalletClient({ account: privateKeyToAccount(`0x${privateKey}`), chain: getChain(84532), - transport: http() - }) + transport: http(), + }); - const paymasterUrlBase = paymasterUrlTwo + const paymasterUrlBase = paymasterUrlTwo; const multiChainModule = await createMultiChainValidationModule({ signer: walletClient, - moduleAddress: DEFAULT_MULTICHAIN_MODULE - }) + moduleAddress: DEFAULT_MULTICHAIN_MODULE, + }); const [polygonAccount, baseAccount] = await Promise.all([ createSmartAccountClient({ @@ -413,7 +416,7 @@ describe("Modules:Write", () => { bundlerUrl, defaultValidationModule: multiChainModule, activeValidationModule: multiChainModule, - paymasterUrl + paymasterUrl, }), createSmartAccountClient({ chainId: chainIdBase, @@ -421,94 +424,94 @@ describe("Modules:Write", () => { bundlerUrl: bundlerUrlBase, defaultValidationModule: multiChainModule, activeValidationModule: multiChainModule, - paymasterUrl: paymasterUrlBase - }) - ]) + paymasterUrl: paymasterUrlBase, + }), + ]); // Check if the smart account has been deployed const [isPolygonDeployed, isBaseDeployed] = await Promise.all([ polygonAccount.isAccountDeployed(), - baseAccount.isAccountDeployed() - ]) + baseAccount.isAccountDeployed(), + ]); if (!isPolygonDeployed) { - const { wait } = await polygonAccount.deploy(withSponsorship) - const { success } = await wait() - expect(success).toBe("true") + const { wait } = await polygonAccount.deploy(withSponsorship); + const { success } = await wait(); + expect(success).toBe("true"); } if (!isBaseDeployed) { - const { wait } = await baseAccount.deploy(withSponsorship) - const { success } = await wait() - expect(success).toBe("true") + const { wait } = await baseAccount.deploy(withSponsorship); + const { success } = await wait(); + expect(success).toBe("true"); } const moduleEnabled1 = await polygonAccount.isModuleEnabled( - DEFAULT_MULTICHAIN_MODULE - ) - const moduleActive1 = polygonAccount.activeValidationModule - expect(moduleEnabled1).toBeTruthy() - expect(moduleActive1.getAddress()).toBe(DEFAULT_MULTICHAIN_MODULE) + DEFAULT_MULTICHAIN_MODULE, + ); + const moduleActive1 = polygonAccount.activeValidationModule; + expect(moduleEnabled1).toBeTruthy(); + expect(moduleActive1.getAddress()).toBe(DEFAULT_MULTICHAIN_MODULE); const moduleEnabled2 = await baseAccount.isModuleEnabled( - DEFAULT_MULTICHAIN_MODULE - ) - const moduleActive2 = polygonAccount.activeValidationModule - expect(moduleEnabled2).toBeTruthy() - expect(moduleActive2.getAddress()).toBe(DEFAULT_MULTICHAIN_MODULE) + DEFAULT_MULTICHAIN_MODULE, + ); + const moduleActive2 = polygonAccount.activeValidationModule; + expect(moduleEnabled2).toBeTruthy(); + expect(moduleActive2.getAddress()).toBe(DEFAULT_MULTICHAIN_MODULE); const encodedCall = encodeFunctionData({ abi: parseAbi([ - "function safeMint(address owner) view returns (uint balance)" + "function safeMint(address owner) view returns (uint balance)", ]), functionName: "safeMint", - args: [recipient] - }) + args: [recipient], + }); const transaction = { to: nftAddress, - data: encodedCall - } + data: encodedCall, + }; const [partialUserOp1, partialUserOp2] = await Promise.all([ baseAccount.buildUserOp([transaction], withSponsorship), - polygonAccount.buildUserOp([transaction], withSponsorship) - ]) + polygonAccount.buildUserOp([transaction], withSponsorship), + ]); - expect(partialUserOp1.paymasterAndData).not.toBe("0x") - expect(partialUserOp2.paymasterAndData).not.toBe("0x") + expect(partialUserOp1.paymasterAndData).not.toBe("0x"); + expect(partialUserOp2.paymasterAndData).not.toBe("0x"); // Sign the user ops using multiChainModule const returnedOps = await multiChainModule.signUserOps([ { userOp: partialUserOp1, chainId: chainIdBase }, - { userOp: partialUserOp2, chainId } - ]) + { userOp: partialUserOp2, chainId }, + ]); // Send the signed user ops on both chains const userOpResponse1 = await baseAccount.sendSignedUserOp( // biome-ignore lint/suspicious/noExplicitAny: - returnedOps[0] as any - ) + returnedOps[0] as any, + ); const userOpResponse2 = await polygonAccount.sendSignedUserOp( // biome-ignore lint/suspicious/noExplicitAny: - returnedOps[1] as any - ) + returnedOps[1] as any, + ); - Logger.log(userOpResponse1.userOpHash, "MULTICHAIN BASE USER OP HASH") - Logger.log(userOpResponse2.userOpHash, "MULTICHAIN POLYGON USER OP HASH") + Logger.log(userOpResponse1.userOpHash, "MULTICHAIN BASE USER OP HASH"); + Logger.log(userOpResponse2.userOpHash, "MULTICHAIN POLYGON USER OP HASH"); - expect(userOpResponse1.userOpHash).toBeTruthy() - expect(userOpResponse2.userOpHash).toBeTruthy() + expect(userOpResponse1.userOpHash).toBeTruthy(); + expect(userOpResponse2.userOpHash).toBeTruthy(); - const { success: success1 } = await userOpResponse1.wait() - const { success: success2 } = await userOpResponse2.wait() + const { success: success1 } = await userOpResponse1.wait(); + const { success: success2 } = await userOpResponse2.wait(); - expect(success1).toBe("true") - expect(success2).toBe("true") - }, 50000) + expect(success1).toBe("true"); + expect(success2).toBe("true"); + }, 50000); test("should use SessionValidationModule to send a user op", async () => { - let sessionSigner: WalletClientSigner - const sessionKeyEOA = walletClient.account.address - const recipient = walletClientTwo.account.address + let sessionSigner: WalletClientSigner; + const sessionKeyEOA = walletClient.account.address; + const recipient = walletClientTwo.account.address; // Create smart account let smartAccount = await createSmartAccountClient({ @@ -516,50 +519,50 @@ describe("Modules:Write", () => { signer: walletClient, bundlerUrl, paymasterUrl, - index: 11 // Increasing index to not conflict with other test cases and use a new smart account - }) - const accountAddress = await smartAccount.getAccountAddress() + index: 11, // Increasing index to not conflict with other test cases and use a new smart account + }); + const accountAddress = await smartAccount.getAccountAddress(); const sessionMemoryStorage: SessionMemoryStorage = new SessionMemoryStorage( - accountAddress - ) + accountAddress, + ); // First we need to check if smart account is deployed // if not deployed, send an empty transaction to deploy it - const isDeployed = await smartAccount.isAccountDeployed() + const isDeployed = await smartAccount.isAccountDeployed(); if (!isDeployed) { const { wait } = await smartAccount.deploy({ - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }) - const { success } = await wait() - expect(success).toBe("true") + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + }); + const { success } = await wait(); + expect(success).toBe("true"); } try { sessionSigner = await sessionMemoryStorage.getSignerByKey( sessionKeyEOA, - chain - ) + chain, + ); } catch (error) { sessionSigner = await sessionMemoryStorage.addSigner( { pbKey: sessionKeyEOA, - pvKey: `0x${privateKey}` + pvKey: `0x${privateKey}`, }, - chain - ) + chain, + ); } - expect(sessionSigner).toBeTruthy() + expect(sessionSigner).toBeTruthy(); // Create session module const sessionModule = await createSessionKeyManagerModule({ moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, smartAccountAddress: await smartAccount.getAddress(), - sessionStorageClient: sessionMemoryStorage - }) + sessionStorageClient: sessionMemoryStorage, + }); const functionSelector = slice( toFunctionSelector("safeMint(address)"), 0, - 4 - ) + 4, + ); // Set enabled call on session const sessionKeyData = await getABISVMSessionKeyData(sessionKeyEOA as Hex, { destContract: "0xdd526eba63ef200ed95f0f0fb8993fe3e20a23d0" as Hex, // nft address @@ -570,101 +573,101 @@ describe("Modules:Write", () => { offset: 0, // offset 0 means we are checking first parameter of safeMint (recipient address) condition: 0, // 0 = Condition.EQUAL referenceValue: pad("0xd3C85Fdd3695Aee3f0A12B3376aCD8DC54020549", { - size: 32 - }) // recipient address - } - ] - }) - const abiSvmAddress = "0x000006bC2eCdAe38113929293d241Cf252D91861" + size: 32, + }), // recipient address + }, + ], + }); + const abiSvmAddress = "0x000006bC2eCdAe38113929293d241Cf252D91861"; const sessionTxData = await sessionModule.createSessionData([ { validUntil: 0, validAfter: 0, sessionValidationModule: abiSvmAddress, sessionPublicKey: sessionKeyEOA as Hex, - sessionKeyData: sessionKeyData as Hex - } - ]) + sessionKeyData: sessionKeyData as Hex, + }, + ]); const setSessionAllowedTrx = { to: DEFAULT_SESSION_KEY_MANAGER_MODULE, - data: sessionTxData.data - } + data: sessionTxData.data, + }; // biome-ignore lint/suspicious/noExplicitAny: - const txArray: any = [] + const txArray: any = []; // Check if module is enabled const isEnabled = await smartAccount.isModuleEnabled( - DEFAULT_SESSION_KEY_MANAGER_MODULE - ) + DEFAULT_SESSION_KEY_MANAGER_MODULE, + ); if (!isEnabled) { const enableModuleTrx = await smartAccount.getEnableModuleData( - DEFAULT_SESSION_KEY_MANAGER_MODULE - ) - txArray.push(enableModuleTrx) - txArray.push(setSessionAllowedTrx) + DEFAULT_SESSION_KEY_MANAGER_MODULE, + ); + txArray.push(enableModuleTrx); + txArray.push(setSessionAllowedTrx); } else { - Logger.log("MODULE ALREADY ENABLED") - txArray.push(setSessionAllowedTrx) + Logger.log("MODULE ALREADY ENABLED"); + txArray.push(setSessionAllowedTrx); } const userOp = await smartAccount.buildUserOp(txArray, { nonceOptions, paymasterServiceData: { - mode: PaymasterMode.SPONSORED - } - }) - const userOpResponse1 = await smartAccount.sendUserOp(userOp) - const transactionDetails = await userOpResponse1.wait() - expect(transactionDetails.success).toBe("true") - Logger.log("Tx Hash: ", transactionDetails.receipt.transactionHash) + mode: PaymasterMode.SPONSORED, + }, + }); + const userOpResponse1 = await smartAccount.sendUserOp(userOp); + const transactionDetails = await userOpResponse1.wait(); + expect(transactionDetails.success).toBe("true"); + Logger.log("Tx Hash: ", transactionDetails.receipt.transactionHash); const encodedCall = encodeFunctionData({ abi: parseAbi(["function safeMint(address _to)"]), functionName: "safeMint", - args: ["0xd3C85Fdd3695Aee3f0A12B3376aCD8DC54020549"] - }) + args: ["0xd3C85Fdd3695Aee3f0A12B3376aCD8DC54020549"], + }); const nftMintTx = { to: "0xdd526eba63ef200ed95f0f0fb8993fe3e20a23d0", - data: encodedCall - } - smartAccount = smartAccount.setActiveValidationModule(sessionModule) - const maticBalanceBefore = await checkBalance(smartAccountAddress) + data: encodedCall, + }; + smartAccount = smartAccount.setActiveValidationModule(sessionModule); + const maticBalanceBefore = await checkBalance(smartAccountAddress); const userOpResponse2 = await smartAccount.sendTransaction(nftMintTx, { nonceOptions, params: { sessionSigner: sessionSigner, - sessionValidationModule: abiSvmAddress + sessionValidationModule: abiSvmAddress, }, paymasterServiceData: { - mode: PaymasterMode.SPONSORED - } - }) - expect(userOpResponse2.userOpHash).toBeTruthy() - expect(userOpResponse2.userOpHash).not.toBeNull() - const maticBalanceAfter = await checkBalance(smartAccountAddress) - expect(maticBalanceAfter).toEqual(maticBalanceBefore) - }, 60000) + mode: PaymasterMode.SPONSORED, + }, + }); + expect(userOpResponse2.userOpHash).toBeTruthy(); + expect(userOpResponse2.userOpHash).not.toBeNull(); + const maticBalanceAfter = await checkBalance(smartAccountAddress); + expect(maticBalanceAfter).toEqual(maticBalanceBefore); + }, 60000); test("should enable batched module", async () => { const smartAccount = await createSmartAccountClient({ signer: walletClient, bundlerUrl, - paymasterUrl - }) + paymasterUrl, + }); const isBRMenabled = await smartAccount.isModuleEnabled( - DEFAULT_BATCHED_SESSION_ROUTER_MODULE - ) + DEFAULT_BATCHED_SESSION_ROUTER_MODULE, + ); if (!isBRMenabled) { const tx = await smartAccount.getEnableModuleData( - DEFAULT_BATCHED_SESSION_ROUTER_MODULE - ) + DEFAULT_BATCHED_SESSION_ROUTER_MODULE, + ); const { wait } = await smartAccount.sendTransaction(tx, { nonceOptions, - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }) - const { success } = await wait() - expect(success).toBe("true") + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + }); + const { success } = await wait(); + expect(success).toBe("true"); } - }, 50000) + }, 50000); test.skip("should use ABI SVM to allow transfer ownership of smart account", async () => { const smartAccount = await createSmartAccountClient({ @@ -672,19 +675,19 @@ describe("Modules:Write", () => { signer: walletClient, bundlerUrl, paymasterUrl, - index: 10 // Increasing index to not conflict with other test cases and use a new smart account - }) + index: 10, // Increasing index to not conflict with other test cases and use a new smart account + }); const smartAccountAddressForPreviousOwner = - await smartAccount.getAccountAddress() + await smartAccount.getAccountAddress(); - const signerOfAccount = walletClient.account.address + const signerOfAccount = walletClient.account.address; const ownerOfAccount = await publicClient.readContract({ address: DEFAULT_ECDSA_OWNERSHIP_MODULE, abi: ECDSAModuleAbi, functionName: "getOwner", - args: [await smartAccount.getAccountAddress()] - }) + args: [await smartAccount.getAccountAddress()], + }); if (ownerOfAccount !== signerOfAccount) { // Re-create the smart account instance with the new owner @@ -693,63 +696,63 @@ describe("Modules:Write", () => { signer: walletClientTwo, bundlerUrl, paymasterUrl, - accountAddress: smartAccountAddressForPreviousOwner - }) + accountAddress: smartAccountAddressForPreviousOwner, + }); // Transfer ownership back to walletClient 1 await smartAccountWithOtherOwner.transferOwnership( walletClient.account.address, DEFAULT_ECDSA_OWNERSHIP_MODULE as TransferOwnershipCompatibleModule, - { paymasterServiceData: { mode: PaymasterMode.SPONSORED } } - ) + { paymasterServiceData: { mode: PaymasterMode.SPONSORED } }, + ); } - let sessionSigner: WalletClientSigner - const sessionKeyEOA = walletClient.account.address - const newOwner = walletClientTwo.account.address + let sessionSigner: WalletClientSigner; + const sessionKeyEOA = walletClient.account.address; + const newOwner = walletClientTwo.account.address; - const accountAddress = await smartAccount.getAccountAddress() + const accountAddress = await smartAccount.getAccountAddress(); const sessionMemoryStorage: SessionMemoryStorage = new SessionMemoryStorage( - accountAddress - ) + accountAddress, + ); // First we need to check if smart account is deployed // if not deployed, send an empty transaction to deploy it - const isDeployed = await smartAccount.isAccountDeployed() + const isDeployed = await smartAccount.isAccountDeployed(); if (!isDeployed) { const { wait } = await smartAccount.deploy({ - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }) - const { success } = await wait() - expect(success).toBe("true") + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + }); + const { success } = await wait(); + expect(success).toBe("true"); } try { sessionSigner = await sessionMemoryStorage.getSignerByKey( sessionKeyEOA, - chain - ) + chain, + ); } catch (error) { sessionSigner = await sessionMemoryStorage.addSigner( { pbKey: sessionKeyEOA, - pvKey: `0x${privateKeyTwo}` + pvKey: `0x${privateKeyTwo}`, }, - chain - ) + chain, + ); } - expect(sessionSigner).toBeTruthy() + expect(sessionSigner).toBeTruthy(); // Create session module const sessionModule = await createSessionKeyManagerModule({ moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, smartAccountAddress: await smartAccount.getAddress(), - sessionStorageClient: sessionMemoryStorage - }) + sessionStorageClient: sessionMemoryStorage, + }); const functionSelectorTransferOwnership = slice( toFunctionSelector("transferOwnership(address) public"), 0, - 4 - ) + 4, + ); const sessionKeyDataTransferOwnership = await getABISVMSessionKeyData( sessionKeyEOA as Hex, { @@ -761,13 +764,13 @@ describe("Modules:Write", () => { offset: 0, // offset 0 means we are checking first parameter of transferOwnership (recipient address) condition: 0, // 0 = Condition.EQUAL referenceValue: pad(walletClient.account.address, { - size: 32 - }) // new owner address - } - ] - } - ) - const abiSvmAddress = "0x000006bC2eCdAe38113929293d241Cf252D91861" + size: 32, + }), // new owner address + }, + ], + }, + ); + const abiSvmAddress = "0x000006bC2eCdAe38113929293d241Cf252D91861"; const sessionTxDataTransferOwnership = await sessionModule.createSessionData([ { @@ -775,37 +778,37 @@ describe("Modules:Write", () => { validAfter: 0, sessionValidationModule: abiSvmAddress, sessionPublicKey: sessionKeyEOA as Hex, - sessionKeyData: sessionKeyDataTransferOwnership as Hex - } - ]) + sessionKeyData: sessionKeyDataTransferOwnership as Hex, + }, + ]); const setSessionAllowedTransferOwnerhsipTrx = { to: DEFAULT_SESSION_KEY_MANAGER_MODULE, - data: sessionTxDataTransferOwnership.data - } + data: sessionTxDataTransferOwnership.data, + }; // biome-ignore lint/suspicious/noExplicitAny: - const txArray: any = [] + const txArray: any = []; // Check if module is enabled const isEnabled = await smartAccount.isModuleEnabled( - DEFAULT_SESSION_KEY_MANAGER_MODULE - ) + DEFAULT_SESSION_KEY_MANAGER_MODULE, + ); if (!isEnabled) { const enableModuleTrx = await smartAccount.getEnableModuleData( - DEFAULT_SESSION_KEY_MANAGER_MODULE - ) - txArray.push(enableModuleTrx) - txArray.push(setSessionAllowedTransferOwnerhsipTrx) + DEFAULT_SESSION_KEY_MANAGER_MODULE, + ); + txArray.push(enableModuleTrx); + txArray.push(setSessionAllowedTransferOwnerhsipTrx); } else { - Logger.log("MODULE ALREADY ENABLED") - txArray.push(setSessionAllowedTransferOwnerhsipTrx) + Logger.log("MODULE ALREADY ENABLED"); + txArray.push(setSessionAllowedTransferOwnerhsipTrx); } const userOpResponse1 = await smartAccount.sendTransaction(txArray, { nonceOptions, - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }) - const transactionDetails = await userOpResponse1.wait() - expect(transactionDetails.success).toBe("true") - Logger.log("Tx Hash: ", transactionDetails.receipt.transactionHash) + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + }); + const transactionDetails = await userOpResponse1.wait(); + expect(transactionDetails.success).toBe("true"); + Logger.log("Tx Hash: ", transactionDetails.receipt.transactionHash); // Transfer ownership back to walletClient await smartAccount.transferOwnership( @@ -815,34 +818,34 @@ describe("Modules:Write", () => { paymasterServiceData: { mode: PaymasterMode.SPONSORED }, params: { sessionSigner: sessionSigner, - sessionValidationModule: abiSvmAddress - } - } - ) - }, 60000) + sessionValidationModule: abiSvmAddress, + }, + }, + ); + }, 60000); test.skip("should correctly parse the reference value and explicitly pass the storage client while resuming the session", async () => { const byteCode = await publicClient.getBytecode({ - address: DUMMY_CONTRACT_ADDRESS as Hex - }) + address: DUMMY_CONTRACT_ADDRESS as Hex, + }); - expect(byteCode).toBeTruthy() + expect(byteCode).toBeTruthy(); const { sessionKeyAddress, sessionStorageClient } = await createSessionKeyEOA( smartAccount, chain, - new SessionMemoryStorage(smartAccountAddress) - ) + new SessionMemoryStorage(smartAccountAddress), + ); - const order = parseAbi(["function submitOrder(uint256 _orderNum)"]) - const cancel = parseAbi(["function submitCancel(uint256 _orderNum)"]) + const order = parseAbi(["function submitOrder(uint256 _orderNum)"]); + const cancel = parseAbi(["function submitCancel(uint256 _orderNum)"]); const sessionBatch: CreateSessionDataParams[] = [ createABISessionDatum({ interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: DUMMY_CONTRACT_ADDRESS, @@ -851,15 +854,15 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, - referenceValue: BigInt(1) - } + referenceValue: BigInt(1), + }, ], - valueLimit: 0n + valueLimit: 0n, }), createABISessionDatum({ interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: DUMMY_CONTRACT_ADDRESS, @@ -868,103 +871,103 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, - referenceValue: BigInt(1) - } + referenceValue: BigInt(1), + }, ], - valueLimit: 0n - }) - ] + valueLimit: 0n, + }), + ]; const { wait, session } = await createBatchSession( smartAccount, sessionStorageClient, sessionBatch, - withSponsorship - ) + withSponsorship, + ); const { receipt: { transactionHash }, - success - } = await wait() + success, + } = await wait(); - expect(success).toBe("true") - expect(transactionHash).toBeTruthy() + expect(success).toBe("true"); + expect(transactionHash).toBeTruthy(); const smartAccountWithSession = await createSessionSmartAccountClient( { accountAddress: await smartAccount.getAccountAddress(), // Set the account address on behalf of the user bundlerUrl, paymasterUrl, - chainId: chain.id + chainId: chain.id, }, sessionStorageClient, // Storage client, full Session or smartAccount address if using default storage - true - ) + true, + ); const submitCancelTx: Transaction = { to: DUMMY_CONTRACT_ADDRESS, data: encodeFunctionData({ abi: cancel, functionName: "submitCancel", - args: [BigInt(1)] - }) - } + args: [BigInt(1)], + }), + }; const submitOrderTx: Transaction = { to: DUMMY_CONTRACT_ADDRESS, data: encodeFunctionData({ abi: order, functionName: "submitOrder", - args: [BigInt(1)] - }) - } + args: [BigInt(1)], + }), + }; - const txs = [submitOrderTx, submitCancelTx] - const correspondingIndexes = [1, 0] // The order of the txs from the sessionBatch + const txs = [submitOrderTx, submitCancelTx]; + const correspondingIndexes = [1, 0]; // The order of the txs from the sessionBatch const batchSessionParams = await getBatchSessionTxParams( txs, correspondingIndexes, sessionStorageClient, - chain - ) + chain, + ); const { wait: waitForTx } = await smartAccountWithSession.sendTransaction( txs, { ...batchSessionParams, ...withSponsorship, - nonceOptions - } - ) + nonceOptions, + }, + ); - const { success: txSuccess } = await waitForTx() - expect(txSuccess).toBe("true") - }, 60000) + const { success: txSuccess } = await waitForTx(); + expect(txSuccess).toBe("true"); + }, 60000); test("should revoke sessions", async () => { - const abiSvmAddress = "0x000006bC2eCdAe38113929293d241Cf252D91861" + const abiSvmAddress = "0x000006bC2eCdAe38113929293d241Cf252D91861"; const byteCode = await publicClient.getBytecode({ - address: DUMMY_CONTRACT_ADDRESS as Hex - }) + address: DUMMY_CONTRACT_ADDRESS as Hex, + }); - expect(byteCode).toBeTruthy() + expect(byteCode).toBeTruthy(); const { sessionKeyAddress, sessionStorageClient } = - await createSessionKeyEOA(smartAccount, chain) + await createSessionKeyEOA(smartAccount, chain); const setMerkleRoot = parseAbi([ - "function setMerkleRoot(bytes32 _merkleRoot)" - ]) - const cancel = parseAbi(["function submitCancel(uint256 _orderNum)"]) - const order = parseAbi(["function submitOrder(uint256 _orderNum)"]) - const setId = parseAbi(["function setId(uint256 _id)"]) + "function setMerkleRoot(bytes32 _merkleRoot)", + ]); + const cancel = parseAbi(["function submitCancel(uint256 _orderNum)"]); + const order = parseAbi(["function submitOrder(uint256 _orderNum)"]); + const setId = parseAbi(["function setId(uint256 _id)"]); const policy: Policy[] = [ { interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: DUMMY_CONTRACT_ADDRESS, @@ -973,15 +976,15 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, - referenceValue: BigInt(1) - } + referenceValue: BigInt(1), + }, ], - valueLimit: 0n + valueLimit: 0n, }, { interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: DUMMY_CONTRACT_ADDRESS, @@ -990,15 +993,15 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, - referenceValue: BigInt(1) - } + referenceValue: BigInt(1), + }, ], - valueLimit: 0n + valueLimit: 0n, }, { interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: DUMMY_CONTRACT_ADDRESS, @@ -1007,38 +1010,38 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, - referenceValue: BigInt(1) - } + referenceValue: BigInt(1), + }, ], - valueLimit: 0n + valueLimit: 0n, }, { interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, functionSelector: setMerkleRoot[0], rules: [], - valueLimit: 0n - } - ] + valueLimit: 0n, + }, + ]; const { wait } = await createSession( smartAccount, policy, sessionStorageClient, - withSponsorship - ) + withSponsorship, + ); const { receipt: { transactionHash }, - success - } = await wait() + success, + } = await wait(); - expect(success).toBe("true") - expect(transactionHash).toBeTruthy() + expect(success).toBe("true"); + expect(transactionHash).toBeTruthy(); const smartAccountWithSession = await createSessionSmartAccountClient( { @@ -1046,72 +1049,71 @@ describe("Modules:Write", () => { bundlerUrl, paymasterUrl, chainId, - index: 25 // Increasing index to not conflict with other test cases and use a new smart account + index: 25, // Increasing index to not conflict with other test cases and use a new smart account }, - sessionStorageClient - ) + sessionStorageClient, + ); const submitCancelTx: Transaction = { to: DUMMY_CONTRACT_ADDRESS, data: encodeFunctionData({ abi: cancel, functionName: "submitCancel", - args: [BigInt(1)] - }) - } + args: [BigInt(1)], + }), + }; const sessionModule = await createSessionKeyManagerModule({ moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, smartAccountAddress, - sessionStorageClient - }) + sessionStorageClient, + }); const allSessionIds = (await sessionStorageClient.getAllSessionData()).map( - ({ sessionID }) => sessionID - ) + ({ sessionID }) => sessionID, + ); // generate a new merkle root const newMerkleRoot = await sessionModule.revokeSessions([ allSessionIds[0] ?? "0x", - allSessionIds[2] ?? "0x" - ]) + allSessionIds[2] ?? "0x", + ]); const submitSetMerkleRootTx: Transaction = { to: DEFAULT_SESSION_KEY_MANAGER_MODULE, data: encodeFunctionData({ abi: setMerkleRoot, functionName: "setMerkleRoot", - args: [newMerkleRoot as Hex] - }) - } + args: [newMerkleRoot as Hex], + }), + }; const { wait: waitForSetMerkleRoot } = await smartAccountWithSession.sendTransaction(submitSetMerkleRootTx, { ...withSponsorship, params: { sessionID: allSessionIds[3] ?? "0x", - sessionValidationModule: abiSvmAddress - } - }) + sessionValidationModule: abiSvmAddress, + }, + }); - const { success: txSuccess } = await waitForSetMerkleRoot() - expect(txSuccess).toBe("true") + const { success: txSuccess } = await waitForSetMerkleRoot(); + expect(txSuccess).toBe("true"); - const sessionDataAfter = await sessionStorageClient.getAllSessionData() - console.log(sessionDataAfter, "sessionDataAfter") + const sessionDataAfter = await sessionStorageClient.getAllSessionData(); const revokedSession = sessionDataAfter.find( - (session) => session.status === "REVOKED" - ) - expect(revokedSession?.sessionID === allSessionIds[0]) + (session) => session.status === "REVOKED", + ); + expect(revokedSession?.sessionID === allSessionIds[0]); const submitOrderTx: Transaction = { to: DUMMY_CONTRACT_ADDRESS, data: encodeFunctionData({ abi: order, functionName: "submitOrder", - args: [BigInt(1)] - }) - } + args: [BigInt(1)], + }), + }; const { wait: waitOrder } = await smartAccount.sendTransaction( submitOrderTx, @@ -1119,21 +1121,21 @@ describe("Modules:Write", () => { paymasterServiceData: { mode: PaymasterMode.SPONSORED }, params: { sessionID: allSessionIds[2] ?? "0x", - sessionValidationModule: abiSvmAddress - } - } - ) + sessionValidationModule: abiSvmAddress, + }, + }, + ); - await waitOrder() + await waitOrder(); const setIdTx: Transaction = { to: DUMMY_CONTRACT_ADDRESS, data: encodeFunctionData({ abi: setId, functionName: "setId", - args: [BigInt(1)] - }) - } + args: [BigInt(1)], + }), + }; // Expect to throw because it has been revoked await expect( @@ -1141,10 +1143,10 @@ describe("Modules:Write", () => { ...withSponsorship, params: { sessionID: allSessionIds[0] ?? "0x", - sessionValidationModule: abiSvmAddress - } - }) - ).rejects.toThrow() + sessionValidationModule: abiSvmAddress, + }, + }), + ).rejects.toThrow(); // Expect to throw because it has been revoked await expect( @@ -1152,37 +1154,37 @@ describe("Modules:Write", () => { ...withSponsorship, params: { sessionID: allSessionIds[2] ?? "0x", - sessionValidationModule: abiSvmAddress - } - }) - ).rejects.toThrow() - }, 60000) + sessionValidationModule: abiSvmAddress, + }, + }), + ).rejects.toThrow(); + }, 60000); test("should combine erc20 token gas payments with a batch session", async () => { - await nonZeroBalance(smartAccountAddress, preferredToken) + await nonZeroBalance(smartAccountAddress, preferredToken); const balanceOfPreferredTokenBefore = await checkBalance( smartAccountAddress, - preferredToken - ) + preferredToken, + ); const { sessionKeyAddress, sessionStorageClient } = - await createSessionKeyEOA(smartAccount, chain) + await createSessionKeyEOA(smartAccount, chain); const maxUnit256Value = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; const approval = parseAbi([ - "function approve(address spender, uint256 value) external returns (bool)" - ]) + "function approve(address spender, uint256 value) external returns (bool)", + ]); const safeMint = parseAbi([ - "function safeMint(address owner) view returns (uint balance)" - ]) + "function safeMint(address owner) view returns (uint balance)", + ]); const leaves: CreateSessionDataParams[] = [ createABISessionDatum({ interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: preferredToken, @@ -1191,20 +1193,20 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, // equal - referenceValue: BICONOMY_TOKEN_PAYMASTER + referenceValue: BICONOMY_TOKEN_PAYMASTER, }, { offset: 32, condition: 1, // less than or equal - referenceValue: maxUnit256Value // max amount - } + referenceValue: maxUnit256Value, // max amount + }, ], - valueLimit: 0n + valueLimit: 0n, }), createABISessionDatum({ interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: nftAddress, @@ -1213,65 +1215,65 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, - referenceValue: smartAccountAddress - } + referenceValue: smartAccountAddress, + }, ], - valueLimit: 0n - }) - ] + valueLimit: 0n, + }), + ]; const { wait, session } = await createBatchSession( smartAccount, sessionStorageClient, leaves, - withSponsorship - ) + withSponsorship, + ); const { receipt: { transactionHash }, - success - } = await wait() + success, + } = await wait(); - expect(success).toBe("true") - expect(transactionHash).toBeTruthy() + expect(success).toBe("true"); + expect(transactionHash).toBeTruthy(); const smartAccountWithSession = await createSessionSmartAccountClient( { accountAddress: smartAccountAddress, // Set the account address on behalf of the user bundlerUrl, paymasterUrl, - chainId: chain.id + chainId: chain.id, }, session, - true // for batching - ) + true, // for batching + ); const approvalTx = { to: preferredToken, data: encodeFunctionData({ abi: approval, functionName: "approve", - args: [BICONOMY_TOKEN_PAYMASTER, 1000000n] // Must be more than the expected value, could be retrieved from the getTokenFees() method - }) - } + args: [BICONOMY_TOKEN_PAYMASTER, 1000000n], // Must be more than the expected value, could be retrieved from the getTokenFees() method + }), + }; const nftMintTx = { to: nftAddress, data: encodeFunctionData({ abi: safeMint, functionName: "safeMint", - args: [smartAccountAddress] - }) - } + args: [smartAccountAddress], + }), + }; - const txs = [approvalTx, nftMintTx] + const txs = [approvalTx, nftMintTx]; const batchSessionParams = await getBatchSessionTxParams( txs, [0, 1], session, - chain - ) + chain, + ); const { wait: waitForMint } = await smartAccountWithSession.sendTransaction( txs, @@ -1279,52 +1281,52 @@ describe("Modules:Write", () => { paymasterServiceData: { mode: PaymasterMode.ERC20, preferredToken, - skipPatchCallData: true // This omits the automatic patching of the call data with approvals + skipPatchCallData: true, // This omits the automatic patching of the call data with approvals }, - ...batchSessionParams - } - ) + ...batchSessionParams, + }, + ); const { receipt: { transactionHash: mintTxHash }, userOpHash, - success: mintSuccess - } = await waitForMint() + success: mintSuccess, + } = await waitForMint(); const balanceOfPreferredTokenAfter = await checkBalance( smartAccountAddress, - preferredToken - ) + preferredToken, + ); - expect(mintSuccess).toBe("true") + expect(mintSuccess).toBe("true"); expect( - balanceOfPreferredTokenBefore - balanceOfPreferredTokenAfter - ).toBeGreaterThan(0) - }, 60000) + balanceOfPreferredTokenBefore - balanceOfPreferredTokenAfter, + ).toBeGreaterThan(0); + }, 60000); test("should use different policy leaves from a single session for a) approving token gas payment approvals and b) the main tx", async () => { - await nonZeroBalance(smartAccountAddress, preferredToken) + await nonZeroBalance(smartAccountAddress, preferredToken); const balanceOfPreferredTokenBefore = await checkBalance( smartAccountAddress, - preferredToken - ) + preferredToken, + ); const { sessionKeyAddress, sessionStorageClient } = - await createSessionKeyEOA(smartAccount, chain) + await createSessionKeyEOA(smartAccount, chain); const maxUnit256Value = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; const approval = parseAbi([ - "function approve(address spender, uint256 value) external returns (bool)" - ]) + "function approve(address spender, uint256 value) external returns (bool)", + ]); const safeMint = parseAbi([ - "function safeMint(address owner) view returns (uint balance)" - ]) + "function safeMint(address owner) view returns (uint balance)", + ]); const policy: Policy[] = [ { interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: preferredToken, @@ -1333,20 +1335,20 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, // equal - referenceValue: BICONOMY_TOKEN_PAYMASTER + referenceValue: BICONOMY_TOKEN_PAYMASTER, }, { offset: 32, condition: 1, // less than or equal - referenceValue: maxUnit256Value // max amount - } + referenceValue: maxUnit256Value, // max amount + }, ], - valueLimit: 0n + valueLimit: 0n, }, { interval: { validUntil: 0, - validAfter: 0 + validAfter: 0, }, sessionKeyAddress, contractAddress: nftAddress, @@ -1355,67 +1357,67 @@ describe("Modules:Write", () => { { offset: 0, condition: 0, - referenceValue: smartAccountAddress - } + referenceValue: smartAccountAddress, + }, ], - valueLimit: 0n - } - ] + valueLimit: 0n, + }, + ]; const { wait, session } = await createSession( smartAccount, policy, sessionStorageClient, - withSponsorship - ) + withSponsorship, + ); const { receipt: { transactionHash }, - success - } = await wait() + success, + } = await wait(); - expect(success).toBe("true") - expect(transactionHash).toBeTruthy() + expect(success).toBe("true"); + expect(transactionHash).toBeTruthy(); const smartAccountWithSession = await createSessionSmartAccountClient( { accountAddress: smartAccountAddress, // Set the account address on behalf of the user bundlerUrl, paymasterUrl, - chainId: chain.id + chainId: chain.id, }, - smartAccountAddress - ) + smartAccountAddress, + ); const approvalTx = { to: preferredToken, data: encodeFunctionData({ abi: approval, functionName: "approve", - args: [BICONOMY_TOKEN_PAYMASTER, 1000000n] // Must be more than the expected value, could be retrieved from the getTokenFees() method - }) - } + args: [BICONOMY_TOKEN_PAYMASTER, 1000000n], // Must be more than the expected value, could be retrieved from the getTokenFees() method + }), + }; const nftMintTx = { to: nftAddress, data: encodeFunctionData({ abi: safeMint, functionName: "safeMint", - args: [smartAccountAddress] - }) - } + args: [smartAccountAddress], + }), + }; const singleSessionParamsForApproval = await getSingleSessionTxParams( session, chain, - 0 - ) + 0, + ); const singleSessionParamsForMint = await getSingleSessionTxParams( session, chain, - 1 - ) + 1, + ); const { wait: waitForApprovalTx } = await smartAccountWithSession.sendTransaction(approvalTx, { @@ -1423,11 +1425,11 @@ describe("Modules:Write", () => { paymasterServiceData: { mode: PaymasterMode.ERC20, preferredToken, - skipPatchCallData: true // This omits the automatic patching of the call data with approvals - } - }) - const { success: txApprovalSuccess } = await waitForApprovalTx() - expect(txApprovalSuccess).toBe("true") + skipPatchCallData: true, // This omits the automatic patching of the call data with approvals + }, + }); + const { success: txApprovalSuccess } = await waitForApprovalTx(); + expect(txApprovalSuccess).toBe("true"); const { wait: waitForMintTx } = await smartAccountWithSession.sendTransaction(nftMintTx, { @@ -1435,20 +1437,90 @@ describe("Modules:Write", () => { paymasterServiceData: { mode: PaymasterMode.ERC20, preferredToken, - skipPatchCallData: true // This omits the automatic patching of the call data with approvals - } - }) + skipPatchCallData: true, // This omits the automatic patching of the call data with approvals + }, + }); - const { success: txMintSuccess } = await waitForMintTx() - expect(txMintSuccess).toBe("true") + const { success: txMintSuccess } = await waitForMintTx(); + expect(txMintSuccess).toBe("true"); const balanceOfPreferredTokenAfter = await checkBalance( smartAccountAddress, - preferredToken - ) + preferredToken, + ); expect( - balanceOfPreferredTokenBefore - balanceOfPreferredTokenAfter - ).toBeGreaterThan(0) - }, 80000) -}) + balanceOfPreferredTokenBefore - balanceOfPreferredTokenAfter, + ).toBeGreaterThan(0); + }, 80000); + + test("should create and use a DAN session on behalf of a user", async () => { + const policy: PolicyWithoutSessionKey[] = [ + { + contractAddress: nftAddress, + functionSelector: "safeMint(address)", + rules: [ + { + offset: 0, + condition: 0, + referenceValue: smartAccountAddress, + }, + ], + interval: { + validAfter: 0, + validUntil: 1720530525, + }, + ...PolicyHelpers.NoValueLimit, + }, + ]; + + const { wait, session } = await createDistributedSession( + smartAccount, + policy, + ); + + const { success } = await wait(); + expect(success).toBe("true"); + + const nftMintTx: Transaction = { + to: nftAddress, + data: encodeFunctionData({ + abi: parseAbi(["function safeMint(address _to)"]), + functionName: "safeMint", + args: [smartAccountAddress], + }), + }; + + const nftBalanceBefore = await checkBalance( + smartAccountAddress, + nftAddress, + ); + + const smartAccountWithSession = await createSessionSmartAccountClient( + { + accountAddress: smartAccountAddress, // Set the account address on behalf of the user + bundlerUrl, + paymasterUrl, + chainId, + }, + smartAccountAddress, + "DAN", + ); + + const { wait: waitForMint } = await smartAccountWithSession.sendTransaction( + nftMintTx, + withSponsorship, + { leafIndex: null, store: smartAccountAddress } // Uses the last session leaf and the default storage client + ) + + const { + success: mintSuccess, + receipt: { transactionHash }, + } = await waitForMint(); + + expect(mintSuccess).toBe("true"); + + const nftBalanceAfter = await checkBalance(smartAccountAddress, nftAddress); + expect(nftBalanceAfter - nftBalanceBefore).toBe(1n); + }, 50000); +}); diff --git a/tests/playground/write.test.ts b/tests/playground/write.test.ts index 963eaa6d7..1ee45fc2b 100644 --- a/tests/playground/write.test.ts +++ b/tests/playground/write.test.ts @@ -1,5 +1,5 @@ -import { http, type Hex, createPublicClient, createWalletClient } from "viem" -import { privateKeyToAccount } from "viem/accounts" +import { http, type Hex, createWalletClient } from "viem" +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { beforeAll, describe, expect, test } from "vitest" import { type BiconomySmartAccountV2, @@ -8,7 +8,8 @@ import { import { getConfig } from "../utils" describe("Playground:Write", () => { - const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e" + // const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e" + // const token = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a" const { chain, chainId, @@ -19,16 +20,11 @@ describe("Playground:Write", () => { } = getConfig() const account = privateKeyToAccount(`0x${privateKey}`) const accountTwo = privateKeyToAccount(`0x${privateKeyTwo}`) - const sender = account.address - const recipient = accountTwo.address - const publicClient = createPublicClient({ - chain, - transport: http() - }) + let [smartAccount, smartAccountTwo]: BiconomySmartAccountV2[] = [] let [smartAccountAddress, smartAccountAddressTwo]: Hex[] = [] - const [walletClient, walletClientTwo] = [ + const [walletClient, walletClientTwo, walletClientRandom] = [ createWalletClient({ account, chain, @@ -38,6 +34,11 @@ describe("Playground:Write", () => { account: accountTwo, chain, transport: http() + }), + createWalletClient({ + account: privateKeyToAccount(generatePrivateKey()), + chain, + transport: http() }) ]