Commit 8d72bc50 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into token-select-fix

parents a216fc5a 7ba4e7e0
...@@ -126,14 +126,16 @@ frontend: ...@@ -126,14 +126,16 @@ frontend:
_default: ENC[AES256_GCM,data:cRcbMzOW2AFyDz/lqv4T9SpDCabXdBKLFN9dbq/rFg4=,iv:G9afggfvZ+BpuE5u31KDVfIxhlP38IE28Yt9pMQsd6E=,tag:t1kkjXPolc/58fuQVULLfA==,type:str] _default: ENC[AES256_GCM,data:cRcbMzOW2AFyDz/lqv4T9SpDCabXdBKLFN9dbq/rFg4=,iv:G9afggfvZ+BpuE5u31KDVfIxhlP38IE28Yt9pMQsd6E=,tag:t1kkjXPolc/58fuQVULLfA==,type:str]
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_default: ENC[AES256_GCM,data:LhVaS9L3ujRwznCe6D+edYU5XT4GLXm4jJ4lKGsrzyA=,iv:ibEJAfSrm3ZWtVJ5Du9MagbC6/Tv5L3xQQRjeQ1BGDA=,tag:IqtgpmJPhmHnSgMuIfI/0A==,type:str] _default: ENC[AES256_GCM,data:LhVaS9L3ujRwznCe6D+edYU5XT4GLXm4jJ4lKGsrzyA=,iv:ibEJAfSrm3ZWtVJ5Du9MagbC6/Tv5L3xQQRjeQ1BGDA=,tag:IqtgpmJPhmHnSgMuIfI/0A==,type:str]
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY:
_default: ENC[AES256_GCM,data:JZ+dOLHGXe2vzb380jPuw5weEp5UXPLWlYj2JsCIRZ4bdV3agTbGIw==,iv:gyzp3Bkhlw3JX2/mg1r8IWruY1b57esLrv09+jGkZUM=,tag:0N/XzMJM1hAVp+xlLCJupA==,type:str]
sops: sops:
kms: [] kms: []
gcp_kms: [] gcp_kms: []
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-01-30T18:25:02Z" lastmodified: "2023-02-14T08:03:14Z"
mac: ENC[AES256_GCM,data:osscQyqUAUatyEzeIHCj10+uj1vCzKJC9W0IyChPKR+KQSEW0I2l20nFcSMCb9sGWVNXzQCfltsDTXzHM5nIuzP2qn/qVmwntQcKB4ibGzaE1gshVQA6deQ28UvGkMjkNTKszqtONgc02G1Kl0d7yiigx+jAQDJNT94amwO/OPs=,iv:6g4T3KJTH0u96y+QBjjJPwjuU4+5psd1cIw+AddvWdE=,tag:Z59JO8iZwfbx80wACXk6sA==,type:str] mac: ENC[AES256_GCM,data:B2AkQb4I83dP3UUitRCcrUfzm3nWmcknIUoMWHyYaG9jasnccbr8zZatYdpbvKFcELVTtjhYk6ly5Sx7+6sk2PZm6o7dN3yHG5lSWmnZqNXkwo42GIk/F6vzDdLutZsu8HH8pWHd9y5R272CIPOOh4+Ur0OtwiGgj3Bp1od76qM=,iv:j7aIPflH0FsYhE/iylvBh5nDmVdghhxAFvaeXlR560k=,tag:/oe6OeitIHaZ4TgM7w/0pg==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
...@@ -72,14 +72,16 @@ frontend: ...@@ -72,14 +72,16 @@ frontend:
_default: ENC[AES256_GCM,data:4cTeqxQnGcpzPK4bMqxZpLgMeFSSDbajN/fmb1UunH8=,iv:bPTQfahGfWF1OfArvYQeSQItMa0Ymkt6eUfDZFBQSOY=,tag:8xHe5AUkbH/rl6cOfkVAKg==,type:str] _default: ENC[AES256_GCM,data:4cTeqxQnGcpzPK4bMqxZpLgMeFSSDbajN/fmb1UunH8=,iv:bPTQfahGfWF1OfArvYQeSQItMa0Ymkt6eUfDZFBQSOY=,tag:8xHe5AUkbH/rl6cOfkVAKg==,type:str]
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_default: ENC[AES256_GCM,data:x1CNJWk9wmqxjKWzD62pIb+scdzt5V22SPrXjvmsIR0=,iv:UjcwWfuGk3HDazHT5OcruevkQX/qAXiaHu6uVoJrSmE=,tag:NcCDR3tRULpiGJRpwBK0GQ==,type:str] _default: ENC[AES256_GCM,data:x1CNJWk9wmqxjKWzD62pIb+scdzt5V22SPrXjvmsIR0=,iv:UjcwWfuGk3HDazHT5OcruevkQX/qAXiaHu6uVoJrSmE=,tag:NcCDR3tRULpiGJRpwBK0GQ==,type:str]
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY:
_default: ENC[AES256_GCM,data:MCne89QeuJCrC/xIsYm+8n2dEPIdrd7UTfKm5H3w34nLCrCP5+e4ZA==,iv:NAEBjnkHCuiojGBD49hJGFd5R1jvS6VOEbqMmWQ4mWc=,tag:zYRe8AYbjNwV64gEoGcI3A==,type:str]
sops: sops:
kms: [] kms: []
gcp_kms: [] gcp_kms: []
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-01-30T18:24:47Z" lastmodified: "2023-02-14T08:03:25Z"
mac: ENC[AES256_GCM,data:KYXWcGq3Irq7styUQke+YFlpClE5LzmPLb7tIJcWmTTnksQKZcLibPJIoEBbPLjznJeKpY/AU777CXStihtZPcWeey7NO8VWk2LcVI1+r+/jzj1Gu0bqfIXIqTXFRy46QBjNsbRYMlZRyWlXSKlXXx4Ahp86sHu5nOHJ3oMe1FY=,iv:70iEvTB2J8lZuQzatWg4OKBN2/vYVoNabdJtbp7X8Y8=,tag:SYI8NKMOq67tYPmSWVQiMA==,type:str] mac: ENC[AES256_GCM,data:xGhF8Zc6BEh0TeAlp6UBTGZ6DNIL/d+nO7xNj2xvuHDmxElg816d8jxErbkgJzoNhs5DGbQXmp3pAUL4JPg8x5T+pn0hodvSb7c6zOlfTwumRC+6R6vC+ZOlxbzut4dSvtWxsPmrncWZLg6hyOilps7qvkLCj2d5UC+U7c1uQjc=,iv:OzLsfii8F3VFSSkUaY7ZjolgBbt8oXEtOYTHHMbJtPU=,tag:0rLPkbDmOSxOufCk9iZTDQ==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.4 2.933a.156.156 0 0 0-.155.156v10.844a.467.467 0 0 1-.934 0V3.09A1.089 1.089 0 0 1 6.401 2h7.474a.467.467 0 0 1 .322.137l4.355 4.355a.467.467 0 0 1 .137.33v7.111a.467.467 0 0 1-.933 0V7.29h-3.89a.467.467 0 0 1-.466-.467V2.933h-7Zm7.933.66v2.763h2.763l-2.763-2.763Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M6.4 2.933a.156.156 0 0 0-.155.156v10.844a.467.467 0 0 1-.934 0V3.09A1.089 1.089 0 0 1 6.401 2h7.474a.467.467 0 0 1 .322.137l4.355 4.355a.467.467 0 0 1 .137.33v7.111a.467.467 0 0 1-.933 0V7.29h-3.89a.467.467 0 0 1-.466-.467v-3.89h-7Zm7.933.66v2.763h2.763l-2.763-2.763Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.224 18.185v.628c0 .308-.04.565-.12.771-.067.19-.181.352-.328.465a.794.794 0 0 1-.48.152.8.8 0 0 1-.48-.152 1.026 1.026 0 0 1-.326-.465 2.159 2.159 0 0 1-.12-.77v-.629c0-.31.04-.566.12-.77.068-.19.181-.352.327-.466a.788.788 0 0 1 .48-.155c.18 0 .34.052.479.155.146.113.26.275.329.465.08.205.12.461.12.771Zm.82.625v-.617c0-.454-.07-.844-.21-1.17a1.674 1.674 0 0 0-.602-.758c-.258-.176-.57-.265-.935-.265-.363 0-.675.089-.939.265-.26.173-.47.437-.6.755-.14.326-.21.717-.21 1.173v.616c0 .452.07.841.21 1.17.139.327.339.578.6.754.264.174.576.26.94.26s.676-.086.935-.26c.262-.176.462-.427.601-.753.14-.33.21-.72.21-1.17Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M13.224 18.185v.628c0 .308-.04.565-.12.771-.067.19-.181.352-.328.465a.794.794 0 0 1-.48.152.8.8 0 0 1-.48-.152 1.026 1.026 0 0 1-.326-.465 2.159 2.159 0 0 1-.12-.77v-.629c0-.31.04-.566.12-.77.068-.19.181-.352.327-.466a.788.788 0 0 1 .48-.155c.18 0 .34.052.479.155.146.113.26.275.329.465.08.205.12.461.12.771Zm.82.625v-.617c0-.454-.07-.844-.21-1.17a1.674 1.674 0 0 0-.602-.758c-.258-.176-.57-.265-.935-.265-.363 0-.675.089-.939.265a1.65 1.65 0 0 0-.6.755c-.14.326-.21.717-.21 1.173v.616c0 .452.07.841.21 1.17.139.327.339.578.6.754.264.174.576.26.94.26s.676-.086.935-.26c.262-.176.462-.427.601-.753.14-.33.21-.72.21-1.17Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.22 16.056c.31-.206.671-.306 1.077-.306.406 0 .77.1 1.075.308.306.205.545.51.693.868.155.364.229.788.229 1.267v.616c0 .477-.075.901-.23 1.268v.001a1.923 1.923 0 0 1-.691.862c-.307.207-.67.304-1.076.304-.404 0-.766-.097-1.077-.303h-.001a1.923 1.923 0 0 1-.692-.863c-.156-.367-.23-.793-.23-1.268v-.617c0-.48.074-.905.23-1.27a1.9 1.9 0 0 1 .693-.866Zm1.077.194c-.32 0-.583.078-.8.223a1.403 1.403 0 0 0-.509.642l-.001.004c-.123.287-.19.642-.19 1.074v.616c0 .426.066.781.19 1.073.123.287.294.498.51.643.216.143.479.219.8.219.324 0 .586-.077.797-.219.216-.145.387-.355.51-.643.123-.292.19-.647.19-1.073v-.616c0-.429-.067-.784-.19-1.073v-.002a1.425 1.425 0 0 0-.511-.646h-.001c-.21-.144-.472-.222-.795-.222Zm-.33.898a.78.78 0 0 0-.242.35l-.002.007c-.065.167-.103.39-.103.68v.635c-.006.234.03.466.106.68a.78.78 0 0 0 .24.349c.102.07.215.104.325.102h.011c.11.002.223-.031.325-.102a.768.768 0 0 0 .242-.349l.002-.006c.066-.168.103-.392.103-.68v-.629c0-.29-.037-.514-.102-.68l-.003-.007a.767.767 0 0 0-.244-.35.535.535 0 0 0-.329-.104h-.005a.537.537 0 0 0-.324.104Zm.332-.604a1.037 1.037 0 0 0-.63.203l-.007.005a1.276 1.276 0 0 0-.406.575 2.39 2.39 0 0 0-.136.858v.625a2.41 2.41 0 0 0 .134.857v.001c.083.23.223.433.408.578l.01.008c.185.13.4.2.624.197.224.004.44-.066.625-.198l.008-.006c.187-.144.328-.346.41-.576.093-.243.135-.532.135-.858v-.628a2.39 2.39 0 0 0-.135-.858 1.265 1.265 0 0 0-.41-.576l-.004-.003a1.034 1.034 0 0 0-.626-.204Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M11.22 16.056c.31-.206.671-.306 1.077-.306.406 0 .77.1 1.075.308.306.205.545.51.693.868.155.364.229.788.229 1.267v.616c0 .477-.075.901-.23 1.268v.001a1.923 1.923 0 0 1-.691.862c-.307.207-.67.304-1.076.304-.404 0-.766-.097-1.077-.303h-.001a1.923 1.923 0 0 1-.692-.863c-.156-.367-.23-.793-.23-1.268v-.617c0-.48.074-.905.23-1.27a1.9 1.9 0 0 1 .693-.866Zm1.077.194c-.32 0-.583.078-.8.223a1.403 1.403 0 0 0-.509.642l-.001.004c-.123.287-.19.642-.19 1.074v.616c0 .426.066.781.19 1.073.123.287.294.498.51.643.216.143.479.219.8.219.324 0 .586-.077.797-.219.216-.145.387-.355.51-.643.123-.292.19-.647.19-1.073v-.616c0-.429-.067-.784-.19-1.073v-.002a1.425 1.425 0 0 0-.511-.646h-.001c-.21-.144-.472-.222-.795-.222Zm-.33.898a.78.78 0 0 0-.242.35l-.002.007c-.065.167-.103.39-.103.68v.635c-.006.234.03.466.106.68a.78.78 0 0 0 .24.349c.102.07.215.104.325.102h.011a.55.55 0 0 0 .325-.102.768.768 0 0 0 .242-.349l.002-.006c.066-.168.103-.392.103-.68v-.629c0-.29-.037-.514-.102-.68l-.003-.007a.767.767 0 0 0-.244-.35.535.535 0 0 0-.329-.104h-.005a.537.537 0 0 0-.324.104Zm.332-.604a1.037 1.037 0 0 0-.63.203l-.007.005a1.276 1.276 0 0 0-.406.575 2.39 2.39 0 0 0-.136.858v.625a2.41 2.41 0 0 0 .134.857v.001c.083.23.223.433.408.578l.01.008c.185.13.4.2.624.197.224.004.44-.066.625-.198l.008-.006c.187-.144.328-.346.41-.576.093-.243.135-.532.135-.858v-.628a2.39 2.39 0 0 0-.135-.858 1.265 1.265 0 0 0-.41-.576l-.004-.003a1.034 1.034 0 0 0-.626-.204Z" fill="currentColor"/>
<path d="M17.06 21v-1h1.301a.5.5 0 0 1 .5.5v.5H17.06Zm-.699 0a.5.5 0 0 1-.5-.5v-3.9a.6.6 0 0 1 1.199 0V21h-.699Z" fill="currentColor"/> <path d="M17.06 21v-1h1.301a.5.5 0 0 1 .5.5v.5H17.06Zm-.699 0a.5.5 0 0 1-.5-.5v-3.9a.6.6 0 0 1 1.199 0V21h-.699Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.861 21v-.5a.5.5 0 0 0-.5-.5H17.06v-3.4a.6.6 0 0 0-1.199 0v3.9a.5.5 0 0 0 .5.5h2.5Zm-1.701-1.1v-3.3a.7.7 0 0 0-1.399 0v3.9a.6.6 0 0 0 .6.6h2.6v-.6a.6.6 0 0 0-.6-.6H17.16Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M18.861 21v-.5a.5.5 0 0 0-.5-.5H17.06v-3.4a.6.6 0 0 0-1.199 0v3.9a.5.5 0 0 0 .5.5h2.5Zm-1.701-1.1v-3.3a.7.7 0 0 0-1.399 0v3.9a.6.6 0 0 0 .6.6h2.6v-.6a.6.6 0 0 0-.6-.6H17.16Z" fill="currentColor"/>
<path d="M6.468 17.532c0-.192.067-.316.163-.398.104-.09.286-.166.562-.166.332 0 .655.108.921.307a.484.484 0 0 0 .579-.776A2.516 2.516 0 0 0 7.195 16h-.001c-.45 0-.873.125-1.192.399-.328.28-.502.68-.502 1.133 0 .244.067.464.196.651.124.182.29.309.452.401.29.165.657.262.949.34l.053.013c.339.09.589.163.76.267.135.083.17.15.17.264 0 .25-.086.352-.19.419-.138.089-.372.145-.696.145a1.548 1.548 0 0 1-.92-.307.484.484 0 0 0-.58.776c.433.322.958.498 1.498.499h.002c.402 0 .853-.064 1.218-.298.4-.256.636-.677.636-1.234 0-.532-.287-.878-.635-1.09-.31-.189-.699-.292-1.003-.373l-.012-.003c-.344-.091-.597-.16-.772-.26a.39.39 0 0 1-.132-.106c-.013-.018-.026-.045-.026-.104Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6.468 17.532c0-.192.067-.316.163-.398.104-.09.286-.166.562-.166.332 0 .655.108.921.307a.484.484 0 0 0 .579-.776A2.516 2.516 0 0 0 7.195 16h-.001c-.45 0-.873.125-1.192.399-.328.28-.502.68-.502 1.133 0 .244.067.464.196.651.124.182.29.309.452.401.29.165.657.262.949.34l.053.013c.339.09.589.163.76.267.135.083.17.15.17.264 0 .25-.086.352-.19.419-.138.089-.372.145-.696.145a1.548 1.548 0 0 1-.92-.307.484.484 0 0 0-.58.776A2.52 2.52 0 0 0 7.192 21h.002c.402 0 .853-.064 1.218-.298.4-.256.636-.677.636-1.234 0-.532-.287-.878-.635-1.09-.31-.189-.699-.292-1.003-.373l-.012-.003c-.344-.091-.597-.16-.772-.26a.39.39 0 0 1-.132-.106c-.013-.018-.026-.045-.026-.104Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.4 2.933a.156.156 0 0 0-.155.156v10.844a.467.467 0 0 1-.934 0V3.09A1.089 1.089 0 0 1 6.401 2h7.474a.467.467 0 0 1 .322.137l4.355 4.355a.467.467 0 0 1 .137.33v7.111a.467.467 0 0 1-.933 0V7.29h-3.89a.467.467 0 0 1-.466-.467V2.933h-7Zm7.933.66v2.763h2.763l-2.763-2.763Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M6.4 2.933a.156.156 0 0 0-.155.156v10.844a.467.467 0 0 1-.934 0V3.09A1.089 1.089 0 0 1 6.401 2h7.474a.467.467 0 0 1 .322.137l4.355 4.355a.467.467 0 0 1 .137.33v7.111a.467.467 0 0 1-.933 0V7.29h-3.89a.467.467 0 0 1-.466-.467v-3.89h-7Zm7.933.66v2.763h2.763l-2.763-2.763Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.06 20h1.301a.5.5 0 0 1 .5.5v.5h-2.5a.5.5 0 0 1-.5-.5v-3.9a.6.6 0 0 1 1.199 0V20Z" fill="currentColor"/> <path d="M17.06 20h1.301a.5.5 0 0 1 .5.5v.5h-2.5a.5.5 0 0 1-.5-.5v-3.9a.6.6 0 0 1 1.199 0V20Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.861 21v-.5a.5.5 0 0 0-.5-.5H17.06v-3.4a.6.6 0 0 0-1.199 0v3.9a.5.5 0 0 0 .5.5h2.5Zm-1.701-1.1v-3.3a.7.7 0 0 0-1.399 0v3.9a.6.6 0 0 0 .6.6h2.6v-.6a.6.6 0 0 0-.6-.6H17.16Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M18.861 21v-.5a.5.5 0 0 0-.5-.5H17.06v-3.4a.6.6 0 0 0-1.199 0v3.9a.5.5 0 0 0 .5.5h2.5Zm-1.701-1.1v-3.3a.7.7 0 0 0-1.399 0v3.9a.6.6 0 0 0 .6.6h2.6v-.6a.6.6 0 0 0-.6-.6H17.16Z" fill="currentColor"/>
<path d="M11.18 15.85a.63.63 0 0 1 .63.63v3.131c0 .177.043.291.11.365.066.071.156.113.29.115.135-.002.222-.044.282-.113l.002-.002c.067-.074.11-.188.11-.364V16.48a.63.63 0 0 1 1.26 0v3.288h-.007a1.44 1.44 0 0 1-.213.641l-.001.002a1.592 1.592 0 0 1-.592.543l-.002.001a1.74 1.74 0 0 1-.682.189v.006h-.313v-.006a1.788 1.788 0 0 1-.688-.188l-.003-.002a1.592 1.592 0 0 1-.592-.543v-.002a1.44 1.44 0 0 1-.214-.64h-.007v-3.29a.63.63 0 0 1 .63-.629ZM7.286 16.285l.397 1.229.484-1.3a.558.558 0 0 1 1.046.388l-.983 2.656v1.304a.588.588 0 1 1-1.177 0v-1.304l-.953-2.56a.628.628 0 1 1 1.186-.413Z" fill="currentColor"/> <path d="M11.18 15.85a.63.63 0 0 1 .63.63v3.131c0 .177.043.291.11.365a.378.378 0 0 0 .29.115c.135-.002.222-.044.282-.113l.002-.002c.067-.074.11-.188.11-.364V16.48a.63.63 0 0 1 1.26 0v3.288h-.007a1.44 1.44 0 0 1-.213.641l-.001.002a1.592 1.592 0 0 1-.592.543l-.002.001a1.74 1.74 0 0 1-.682.189v.006h-.313v-.006a1.788 1.788 0 0 1-.688-.188l-.003-.002a1.592 1.592 0 0 1-.592-.543v-.002a1.44 1.44 0 0 1-.214-.64h-.007v-3.29a.63.63 0 0 1 .63-.629Zm-3.894.435.397 1.229.484-1.3a.558.558 0 0 1 1.046.388l-.983 2.656v1.304a.588.588 0 1 1-1.177 0v-1.304l-.953-2.56a.628.628 0 1 1 1.186-.413Z" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.346 13.736c.607 1.073-.75 1.406-1.386 1.467-.965.094-1.165-.464-.976-1.166a1.262 1.262 0 0 1 1.085-.951 1.342 1.342 0 0 1 1.277.65Zm3.787-1.984c-.705 5.417 8.8 4.29 8.618 8.6.938-1.223 1.34-4.55-1.42-6.262-2.458-1.526-5.662-.69-7.198-2.338Zm5.474-2.042c-.061-.058-.125-.114-.187-.17.063.057.125.117.187.17Z" fill="currentColor"/>
<path d="m25.817 13.658-.006-.009a3.601 3.601 0 0 0-.292-.458 2.358 2.358 0 0 0-1.303-.886 5.545 5.545 0 0 0-1.06-.167c-.364-.027-.734-.042-1.108-.061-.75-.042-1.518-.12-2.268-.334a6.304 6.304 0 0 1-1.11-.425 4.745 4.745 0 0 1-.973-.71c-.577-.54-1.031-1.151-1.486-1.745a20.606 20.606 0 0 0-1.378-1.714 6.285 6.285 0 0 0-1.687-1.324 5.812 5.812 0 0 0-2.101-.602 4.947 4.947 0 0 1 2.244.275 5.934 5.934 0 0 1 1.947 1.242c.367.345.712.714 1.032 1.104 2.38-.47 4.312-.052 5.796.76l.034.016a7.31 7.31 0 0 1 1.507 1.092c.316.292.61.606.88.942l.021.027c.877 1.115 1.31 2.274 1.31 2.977Z" fill="currentColor"/>
<path d="m25.816 13.658-.005-.011.005.01ZM11.165 5.92c.608.088 1.228.331 1.627.795.4.463.546 1.066.662 1.64.093.444.168.897.342 1.318.084.205.208.385.311.579.086.161.241.306.301.478a.154.154 0 0 1-.018.153c-.212.235-.783-.026-1-.132a3.254 3.254 0 0 1-.982-.753c-.866-.966-1.313-2.355-1.286-3.62a4.22 4.22 0 0 1 .043-.458Zm10.161 10.887c-1.312 3.679 4.64 6.147 2.41 9.888 2.289-.95 3.375-3.817 2.425-6.092-.83-1.998-3.286-2.726-4.835-3.796Zm-7.873 4.809c.358-.27.749-.493 1.164-.663.42-.17.854-.295 1.299-.377.882-.169 1.755-.21 2.488-.507.362-.142.695-.35.983-.612.279-.26.492-.583.623-.942.133-.378.188-.78.16-1.181a4.348 4.348 0 0 0-.289-1.256 2.883 2.883 0 0 1 .734 2.614 2.71 2.71 0 0 1-.715 1.294c-.35.346-.77.61-1.234.773-.441.155-.902.25-1.37.282-.45.038-.884.048-1.312.074a8.433 8.433 0 0 0-2.53.502Zm8.397 6.469c-.133.105-.265.217-.41.315-.147.097-.3.183-.459.257-.33.162-.695.245-1.063.242-.997-.02-1.702-.765-2.115-1.608-.281-.574-.475-1.195-.809-1.742-.477-.783-1.294-1.413-2.25-1.296-.39.049-.755.225-.972.565-.57.888.248 2.132 1.292 1.956a1.14 1.14 0 0 0 .259-.072.936.936 0 0 0 .23-.14 1.05 1.05 0 0 0 .317-.461c.069-.188.084-.391.044-.587a.806.806 0 0 0-.335-.502c.2.094.356.263.435.47.082.215.104.447.061.672a1.332 1.332 0 0 1-.298.635 1.304 1.304 0 0 1-.281.24 1.638 1.638 0 0 1-1.064.234 1.946 1.946 0 0 1-.947-.413c-.322-.256-.562-.591-.854-.88a4.057 4.057 0 0 0-1.164-.854c-.3-.132-.615-.23-.938-.291a6.176 6.176 0 0 0-.49-.08c-.075-.007-.443-.089-.494-.041a10.29 10.29 0 0 1 1.65-1.243 8.013 8.013 0 0 1 1.935-.832 5.497 5.497 0 0 1 2.164-.166c.374.045.74.14 1.088.282.365.147.702.356.995.618.291.275.526.604.692.97.15.34.262.698.334 1.064.214 1.095.135 2.793 1.563 3.044.074.014.15.025.225.032l.233.006c.16-.012.32-.035.477-.07.326-.076.644-.185.948-.324Z" fill="currentColor"/>
<path d="m13.553 26.89-.037-.029.037.03Zm-1.281-15.456a1.59 1.59 0 0 1-.267.554 2.167 2.167 0 0 1-.889.683c-.315.137-.65.225-.99.262-.075.01-.152.015-.226.02a1.01 1.01 0 0 0-.94.754 2.57 2.57 0 0 0-.057.317c-.034.277-.04.565-.07.914a5.428 5.428 0 0 1-.505 1.706c-.34.718-.72 1.296-.632 2.123.058.537.332.896.696 1.268.656.674 2.125.975 1.797 2.637-.198.991-1.835 2.032-4.135 2.395.229-.035-.294-.919-.326-.975-.247-.388-.517-.754-.713-1.175-.384-.816-.562-1.76-.405-2.655.166-.942.86-1.664 1.436-2.383.687-.856 1.407-1.978 1.566-3.089.037-.27.064-.606.124-.942.057-.371.173-.731.343-1.066.116-.22.269-.417.452-.585a.585.585 0 0 0 .112-.712L4.976 4.86l5.267 6.53a.656.656 0 0 0 1.01.022.448.448 0 0 0 .013-.565c-.344-.442-.708-.892-1.06-1.335L8.88 7.864l-2.66-3.29L2.592 0 6.56 4.278l2.832 3.146L10.806 9c.469.53.937 1.044 1.406 1.601l.077.094.017.146a1.74 1.74 0 0 1-.034.593Zm1.214 15.413a3.535 3.535 0 0 1-.674-.689c.207.247.432.477.674.689Z" fill="currentColor"/>
</svg>
...@@ -13,9 +13,10 @@ export default function fetchFactory( ...@@ -13,9 +13,10 @@ export default function fetchFactory(
// first arg can be only a string // first arg can be only a string
// FIXME migrate to RequestInfo later if needed // FIXME migrate to RequestInfo later if needed
return function fetch(url: string, init?: RequestInit): Promise<Response> { return function fetch(url: string, init?: RequestInit): Promise<Response> {
const incomingContentType = _req.headers['content-type'];
const headers = { const headers = {
accept: 'application/json', accept: 'application/json',
'content-type': 'application/json', 'content-type': incomingContentType?.match(/^multipart\/form-data/) ? incomingContentType : 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`, cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
}; };
...@@ -25,10 +26,23 @@ export default function fetchFactory( ...@@ -25,10 +26,23 @@ export default function fetchFactory(
req: _req, req: _req,
}); });
const body = (() => {
const _body = init?.body;
if (!_body) {
return;
}
if (typeof _body === 'string') {
return _body;
}
return JSON.stringify(_body);
})();
return nodeFetch(url, { return nodeFetch(url, {
...init, ...init,
headers, headers,
body: init?.body ? JSON.stringify(init.body) : undefined, body,
}); });
}; };
} }
...@@ -16,14 +16,14 @@ import type { ...@@ -16,14 +16,14 @@ import type {
import type { AddressesResponse } from 'types/api/addresses'; import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { IndexingStatus } from 'types/api/indexingStatus'; import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search'; import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo, TokenHolders, TokenInventoryResponse, TokenInstance, TokenInstanceTransfersCount } from 'types/api/token';
import type { TokensResponse, TokensFilters } from 'types/api/tokens'; import type { TokensResponse, TokensFilters } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
...@@ -212,6 +212,12 @@ export const RESOURCES = { ...@@ -212,6 +212,12 @@ export const RESOURCES = {
contract_methods_write_proxy: { contract_methods_write_proxy: {
path: '/api/v2/smart-contracts/:id/methods-write-proxy', path: '/api/v2/smart-contracts/:id/methods-write-proxy',
}, },
contract_verification_config: {
path: '/api/v2/smart-contracts/verification/config',
},
contract_verification_via: {
path: '/api/v2/smart-contracts/:id/verification/via/:method',
},
// TOKEN // TOKEN
token: { token: {
...@@ -230,12 +236,25 @@ export const RESOURCES = { ...@@ -230,12 +236,25 @@ export const RESOURCES = {
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
token_inventory: {
path: '/api/v2/tokens/:hash/instances',
paginationFields: [ 'unique_token' as const ],
filterFields: [],
},
tokens: { tokens: {
path: '/api/v2/tokens', path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ], paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ],
filterFields: [ 'filter' as const, 'type' as const ], filterFields: [ 'filter' as const, 'type' as const ],
}, },
// TOKEN INSTANCE
token_instance: {
path: '/api/v2/tokens/:hash/instances/:id',
},
token_instance_transfers_count: {
path: '/api/v2/tokens/:hash/instances/:id/transfers-count',
},
// HOMEPAGE // HOMEPAGE
homepage_stats: { homepage_stats: {
path: '/api/v2/stats', path: '/api/v2/stats',
...@@ -309,7 +328,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -309,7 +328,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_logs' | 'address_tokens' |
'token_transfers' | 'token_holders' | 'tokens'; 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -356,6 +375,9 @@ Q extends 'token' ? TokenInfo : ...@@ -356,6 +375,9 @@ Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters : Q extends 'token_counters' ? TokenCounters :
Q extends 'token_transfers' ? TokenTransferResponse : Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders : Q extends 'token_holders' ? TokenHolders :
Q extends 'token_instance' ? TokenInstance :
Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount :
Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse : Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
...@@ -364,6 +386,7 @@ Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> : ...@@ -364,6 +386,7 @@ Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> : Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> :
Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> : Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> :
Q extends 'visualize_sol2uml' ? VisualizedContract : Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
...@@ -12,3 +12,6 @@ export const DAY = 24 * HOUR; ...@@ -12,3 +12,6 @@ export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY; export const WEEK = 7 * DAY;
export const MONTH = 30 * DAY; export const MONTH = 30 * DAY;
export const YEAR = 365 * DAY; export const YEAR = 365 * DAY;
export const Kb = 1_000;
export const Mb = 1_000 * Kb;
...@@ -141,6 +141,9 @@ function makePolicyMap() { ...@@ -141,6 +141,9 @@ function makePolicyMap() {
// walletconnect // walletconnect
'*.walletconnect.com', '*.walletconnect.com',
// token's media
'ipfs.io',
], ],
'font-src': [ 'font-src': [
......
...@@ -11,7 +11,7 @@ export interface Params { ...@@ -11,7 +11,7 @@ export interface Params {
method?: RequestInit['method']; method?: RequestInit['method'];
headers?: RequestInit['headers']; headers?: RequestInit['headers'];
signal?: RequestInit['signal']; signal?: RequestInit['signal'];
body?: Record<string, unknown>; body?: Record<string, unknown> | FormData;
credentials?: RequestCredentials; credentials?: RequestCredentials;
} }
...@@ -20,13 +20,27 @@ export default function useFetch() { ...@@ -20,13 +20,27 @@ export default function useFetch() {
const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {}; const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => { return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => {
const hasBody = params?.method && ![ 'GET', 'HEAD' ].includes(params.method); const _body = params?.body;
const isFormData = _body instanceof FormData;
const isBodyAllowed = params?.method && ![ 'GET', 'HEAD' ].includes(params.method);
const body: FormData | string | undefined = (() => {
if (!isBodyAllowed) {
return;
}
if (isFormData) {
return _body;
}
return JSON.stringify({ ..._body, _csrf_token: token });
})();
const reqParams = { const reqParams = {
...params, ...params,
body: hasBody ? JSON.stringify({ ...params.body, _csrf_token: token }) : undefined, body,
headers: { headers: {
...(hasBody ? { 'Content-type': 'application/json' } : undefined), ...(isBodyAllowed && !isFormData ? { 'Content-type': 'application/json' } : undefined),
...params?.headers, ...params?.headers,
}, },
}; };
......
// https://hexdocs.pm/phoenix/js/
import type { SocketConnectOption } from 'phoenix'; import type { SocketConnectOption } from 'phoenix';
import { Socket } from 'phoenix'; import { Socket } from 'phoenix';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
......
...@@ -2,6 +2,7 @@ import type { Channel } from 'phoenix'; ...@@ -2,6 +2,7 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -19,6 +20,7 @@ SocketMessage.AddressTxs | ...@@ -19,6 +20,7 @@ SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending | SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer | SocketMessage.AddressTokenTransfer |
SocketMessage.TokenTransfers | SocketMessage.TokenTransfers |
SocketMessage.ContractVerification |
SocketMessage.Unknown; SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
...@@ -43,6 +45,7 @@ export namespace SocketMessage { ...@@ -43,6 +45,7 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }> export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -60,7 +60,11 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on ...@@ -60,7 +60,11 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
} else { } else {
ch = socket.channel(topic); ch = socket.channel(topic);
CHANNEL_REGISTRY[topic] = ch; CHANNEL_REGISTRY[topic] = ch;
ch.join().receive('ok', (message) => onJoinRef.current?.(ch, message)); ch.join()
.receive('ok', (message) => onJoinRef.current?.(ch, message))
.receive('error', () => {
onSocketError?.();
});
} }
setChannel(ch); setChannel(ch);
...@@ -70,7 +74,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on ...@@ -70,7 +74,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
delete CHANNEL_REGISTRY[topic]; delete CHANNEL_REGISTRY[topic];
setChannel(undefined); setChannel(undefined);
}; };
}, [ socket, topic, params, isDisabled ]); }, [ socket, topic, params, isDisabled, onSocketError ]);
return channel; return channel;
} }
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [ const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' }, { title: 'ERC-20', id: 'ERC-20' },
......
import type { TokenHolders } from 'types/api/tokenInfo'; import type { TokenHolders } from 'types/api/token';
import { withName, withoutName } from 'mocks/address/address'; import { withName, withoutName } from 'mocks/address/address';
......
import type { TokenCounters, TokenInfo } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo } from 'types/api/token';
export const tokenInfo: TokenInfo = { export const tokenInfo: TokenInfo = {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
......
import type { TokenInstance } from 'types/api/token';
import * as addressMock from '../address/address';
import { tokenInfoERC721a } from './tokenInfo';
export const base: TokenInstance = {
animation_url: null,
external_app_url: null,
id: '32925298983216553915666621415831103694597106215670571463977478984525997408266',
image_url: null,
is_unique: false,
holder_address_hash: null,
metadata: {
animation_url: null,
description: 'Sign for you!',
external_link: null,
image: 'https://i.seadn.io/gcs/files/1ee1c5e1ead058322615e3206abb8ba3.png?w=500&auto=format',
name: 'Sign4U',
},
owner: addressMock.withName,
token: tokenInfoERC721a,
};
...@@ -64,6 +64,7 @@ export const base: Transaction = { ...@@ -64,6 +64,7 @@ export const base: Transaction = {
], ],
type: 2, type: 2,
value: '42000000000000000000', value: '42000000000000000000',
actions: [],
}; };
export const withContractCreation: Transaction = { export const withContractCreation: Transaction = {
...@@ -170,3 +171,72 @@ export const pending: Transaction = { ...@@ -170,3 +171,72 @@ export const pending: Transaction = {
type: null, type: null,
value: '0', value: '0',
}; };
export const withActionsUniswap: Transaction = {
...base,
actions: [
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '7143.488560357232097378',
amount1: '10',
symbol0: 'XFT',
symbol1: 'Ether',
},
protocol: 'uniswap_v3',
type: 'mint',
},
{
data: {
address: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88',
ids: [
'53699',
'53700123456',
'42',
],
name: 'Uniswap V3: Positions NFT',
symbol: 'UNI-V3-POS',
to: '0x6d872Fb5F5B2B1f71fA9AadE159bc3976c1946B7',
},
protocol: 'uniswap_v3',
type: 'mint_nft',
},
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '42876.488560357232',
amount1: '345.908098203434',
symbol0: 'SHAVUHA',
symbol1: 'BOB',
},
protocol: 'uniswap_v3',
type: 'swap',
},
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '42',
amount1: '0.523523223232',
symbol0: 'VIC',
symbol1: 'USDT',
},
protocol: 'uniswap_v3',
type: 'burn',
},
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '42',
amount1: '0.523523223232',
symbol0: 'BOB',
symbol1: 'UNI',
},
protocol: 'uniswap_v3',
type: 'collect',
},
],
};
...@@ -35,10 +35,13 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -35,10 +35,13 @@ function MyApp({ Component, pageProps }: AppProps) {
}, },
})); }));
const renderErrorScreen = React.useCallback(() => { const renderErrorScreen = React.useCallback((error?: Error) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusCode = (error?.cause as any)?.status || 500;
return ( return (
<AppError <AppError
statusCode={ 500 } statusCode={ statusCode }
height="100vh" height="100vh"
display="flex" display="flex"
flexDirection="column" flexDirection="column"
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from 'lib/next/token/types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import TokenInstance from 'ui/pages/TokenInstance';
const TokenInstancePage: NextPage<PageParams> = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<TokenInstance/>
</>
);
};
export default TokenInstancePage;
export { getServerSideProps } from 'lib/next/token/getServerSideProps';
import { formAnatomy as parts } from '@chakra-ui/anatomy'; import { formAnatomy as parts } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system'; import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { getColor } from '@chakra-ui/theme-tools'; import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors'; import getDefaultFormColors from '../utils/getDefaultFormColors';
import FancySelect from './FancySelect'; import FancySelect from './FancySelect';
...@@ -81,11 +81,18 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction ...@@ -81,11 +81,18 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles, 'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles,
[` [`
input[disabled] + label, input[disabled] + label,
textarea[disabled] + label,
&[aria-disabled=true] label &[aria-disabled=true] label
`]: { `]: {
backgroundColor: 'transparent', backgroundColor: 'transparent',
}, },
// in textarea bg of label could not be transparent; it should match the background color of input but without alpha
// so we have to use non-standard colors here
'textarea[disabled] + label': {
backgroundColor: mode('#ececec', '#232425')(props),
},
'textarea[disabled] + label[data-in-modal=true]': {
backgroundColor: mode('#ececec', '#292b34')(props),
},
// indicator styles // indicator styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': { 'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': {
......
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es6",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
......
...@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction';
import type { AddressTag, WatchlistName } from './addressParams'; import type { AddressTag, WatchlistName } from './addressParams';
import type { Block } from './block'; import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { TokenInfo, TokenType } from './tokenInfo'; import type { TokenInfo, TokenType } from './token';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer'; import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
export interface Address { export interface Address {
......
...@@ -113,3 +113,34 @@ export interface SmartContractQueryMethodReadError { ...@@ -113,3 +113,34 @@ export interface SmartContractQueryMethodReadError {
} }
export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError; export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError;
// VERIFICATION
export type SmartContractVerificationMethod = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part' | 'vyper-code' | 'vyper-multi-part';
export interface SmartContractVerificationConfigRaw {
solidity_compiler_versions: Array<string>;
solidity_evm_versions: Array<string>;
verification_options: Array<string>;
vyper_compiler_versions: Array<string>;
vyper_evm_versions: Array<string>;
}
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
verification_options: Array<SmartContractVerificationMethod>;
}
export type SmartContractVerificationResponse = {
status: 'error';
errors: SmartContractVerificationError;
} | {
status: 'success';
}
export interface SmartContractVerificationError {
contract_source_code?: Array<string>;
files?: Array<string>;
compiler_version?: Array<string>;
constructor_arguments?: Array<string>;
name?: Array<string>;
}
...@@ -34,3 +34,28 @@ export type TokenHoldersPagination = { ...@@ -34,3 +34,28 @@ export type TokenHoldersPagination = {
items_count: number; items_count: number;
value: string; value: string;
} }
export interface TokenInstance {
is_unique: boolean;
id: string;
holder_address_hash: string | null;
image_url: string | null;
animation_url: string | null;
external_app_url: string | null;
metadata: unknown;
owner: AddressParam;
token: TokenInfo;
}
export interface TokenInstanceTransfersCount {
transfers_count: number;
}
export interface TokenInventoryResponse {
items: Array<TokenInstance>;
next_page_params: TokenInventoryPagination;
}
export type TokenInventoryPagination = {
unique_token: number;
}
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric, TokenType } from './tokenInfo'; import type { TokenInfoGeneric, TokenType } from './token';
export type Erc20TotalPayload = { export type Erc20TotalPayload = {
decimals: string | null; decimals: string | null;
......
export type Tokenlist = {
message: string;
result: Array<TokenlistItem> | string;
}
export type TokenlistItem = {
balance: number;
contractAddress: string;
decimals: number | null;
id: number;
name: string;
symbol: string;
type: string;
}
import type { TokenInfo, TokenType } from './tokenInfo'; import type { TokenInfo, TokenType } from './token';
export type TokensResponse = { export type TokensResponse = {
items: Array<TokenInfo>; items: Array<TokenInfo>;
......
...@@ -3,6 +3,7 @@ import type { BlockTransactionsResponse } from './block'; ...@@ -3,6 +3,7 @@ import type { BlockTransactionsResponse } from './block';
import type { DecodedInput } from './decodedInput'; import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee'; import type { Fee } from './fee';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
import type { TxAction } from './txAction';
export type TransactionRevertReason = { export type TransactionRevertReason = {
raw: string; raw: string;
...@@ -48,6 +49,7 @@ export type Transaction = ( ...@@ -48,6 +49,7 @@ export type Transaction = (
method: string | null; method: string | null;
tx_types: Array<TransactionType>; tx_types: Array<TransactionType>;
tx_tag: string | null; tx_tag: string | null;
actions: Array<TxAction>;
} }
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
......
export interface TxActionGeneral {
type: 'mint' | 'burn' | 'collect' | 'swap';
data: {
amount0: string;
symbol0: string;
address0: string;
amount1: string;
symbol1: string;
address1: string;
};
}
export interface TxActionNft {
type: 'mint_nft';
data: {
name: string;
symbol: string;
address: string;
to: string;
ids: Array<string>;
};
}
export type TxAction = {
protocol: 'uniswap_v3';
} & (TxActionGeneral | TxActionNft)
...@@ -9,25 +9,19 @@ import appConfig from 'configs/app/config'; ...@@ -9,25 +9,19 @@ import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import AddressAddToMetaMask from './details/AddressAddToMetaMask';
import AddressBalance from './details/AddressBalance'; import AddressBalance from './details/AddressBalance';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton'; import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressFavoriteButton from './details/AddressFavoriteButton';
import AddressNameInfo from './details/AddressNameInfo'; import AddressNameInfo from './details/AddressNameInfo';
import AddressQrCode from './details/AddressQrCode';
import TokenSelect from './tokenSelect/TokenSelect'; import TokenSelect from './tokenSelect/TokenSelect';
interface Props { interface Props {
...@@ -37,7 +31,6 @@ interface Props { ...@@ -37,7 +31,6 @@ interface Props {
const AddressDetails = ({ addressQuery, scrollRef }: Props) => { const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile();
const addressHash = router.query.id?.toString(); const addressHash = router.query.id?.toString();
...@@ -92,18 +85,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -92,18 +85,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
return ( return (
<Box> <Box>
<Flex alignItems="center"> <AddressHeadingInfo address={ data } token={ data.token } isLinkDisabled/>
<AddressIcon address={ data }/>
<Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }>
{ isMobile ? <HashStringShorten hash={ data.hash }/> : data.hash }
</Text>
<CopyToClipboard text={ data.hash }/>
{ data.is_contract && data.token && <AddressAddToMetaMask ml={ 2 } token={ data.token }/> }
{ !data.is_contract && (
<AddressFavoriteButton hash={ data.hash } isAdded={ Boolean(data.watchlist_names?.length) } ml={ 3 }/>
) }
<AddressQrCode hash={ data.hash } ml={ 2 }/>
</Flex>
{ explorers.length > 0 && ( { explorers.length > 0 && (
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap"> <Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
<Text fontSize="sm">Verify with other explorers</Text> <Text fontSize="sm">Verify with other explorers</Text>
......
...@@ -6,7 +6,7 @@ import React from 'react'; ...@@ -6,7 +6,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address'; import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import crossIcon from 'icons/cross.svg'; import crossIcon from 'icons/cross.svg';
......
...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
......
...@@ -129,7 +129,7 @@ const ContractCode = () => { ...@@ -129,7 +129,7 @@ const ContractCode = () => {
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/> <AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address> </Address>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span> <chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<LinkInternal href={ link('address_contract_verification', { id: data.verified_twin_address_hash }) }>Verify & Publish</LinkInternal> <LinkInternal href={ link('address_contract_verification', { id: addressHash }) }>Verify & Publish</LinkInternal>
<span> page</span> <span> page</span>
</Alert> </Alert>
) } ) }
......
import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react'; import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import metamaskIcon from 'icons/metamask.svg'; import metamaskIcon from 'icons/metamask.svg';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
......
...@@ -4,7 +4,7 @@ import type { ChangeEvent } from 'react'; ...@@ -4,7 +4,7 @@ import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import type { FormattedData } from './types'; import type { FormattedData } from './types';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import arrowIcon from 'icons/arrows/east.svg'; import arrowIcon from 'icons/arrows/east.svg';
import searchIcon from 'icons/search.svg'; import searchIcon from 'icons/search.svg';
......
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import type { TokenEnhancedData } from 'ui/address/utils/tokenUtils'; import type { TokenEnhancedData } from 'ui/address/utils/tokenUtils';
......
import { Center, Flex, Icon, Link, Text, LinkBox, LinkOverlay, useColorModeValue } from '@chakra-ui/react'; import { Flex, Link, Text, LinkBox, LinkOverlay, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import NFTIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link'; import link from 'lib/link/link';
import NftImage from 'ui/shared/nft/NftImage';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
...@@ -15,8 +15,7 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => { ...@@ -15,8 +15,7 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => {
return ( return (
<LinkBox <LinkBox
w={{ base: 'calc((100% - 12px)/2)', lg: '210px' }} w={{ base: '100%', lg: '210px' }}
h={{ base: 'auto', lg: '272px' }}
border="1px solid" border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px" borderRadius="12px"
...@@ -26,16 +25,14 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => { ...@@ -26,16 +25,14 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => {
fontWeight={ 500 } fontWeight={ 500 }
lineHeight="20px" lineHeight="20px"
> >
<LinkOverlay href={ tokenLink }/> <LinkOverlay href={ tokenLink }>
<Center <NftImage
w={{ base: '100%', lg: '182px' }} mb="18px"
h={{ base: 'calc((100vw - 36px)/2 - 12px)', lg: '182px' }} url={ null }
bg={ useColorModeValue('blackAlpha.50', 'whiteAlpha.50') } fallbackPadding="30px"
mb="18px" cursor="pointer"
borderRadius="12px" />
> </LinkOverlay>
<Icon as={ NFTIcon } boxSize="112px" color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') }/>
</Center>
{ tokenId && ( { tokenId && (
<Flex mb={ 2 } ml={ 1 }> <Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text> <Text whiteSpace="pre" variant="secondary">ID# </Text>
......
import { Flex, Skeleton, Text } from '@chakra-ui/react'; import { Grid, Skeleton, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -38,11 +38,18 @@ const TokensWithIds = ({ tokensQuery }: Props) => { ...@@ -38,11 +38,18 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
return ( return (
<> <>
{ bar } { bar }
<Flex columnGap={ 6 } rowGap={ 6 } flexWrap="wrap"> <Grid
<Skeleton w="210px" h="272px"/> w="100%"
<Skeleton w="210px" h="272px"/> columnGap={{ base: 3, lg: 6 }}
<Skeleton w="210px" h="272px"/> rowGap={{ base: 3, lg: 6 }}
</Flex> gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
</> </>
); );
} }
...@@ -54,9 +61,14 @@ const TokensWithIds = ({ tokensQuery }: Props) => { ...@@ -54,9 +61,14 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
return ( return (
<> <>
{ bar } { bar }
<Flex columnGap={{ base: 3, lg: 6 }} rowGap={{ base: 3, lg: 6 }} flexWrap="wrap"> <Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) } { data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) }
</Flex> </Grid>
</> </>
); );
}; };
......
...@@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js'; ...@@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js';
import fpAdd from 'lodash/fp/add'; import fpAdd from 'lodash/fp/add';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import sumBnReducer from 'lib/bigint/sumBnReducer'; import sumBnReducer from 'lib/bigint/sumBnReducer';
import { ZERO } from 'lib/consts'; import { ZERO } from 'lib/consts';
......
...@@ -103,7 +103,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -103,7 +103,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
{ ...field } { ...field }
disabled={ true } disabled={ true }
/> />
<FormLabel>Auto-generated API key token</FormLabel> <FormLabel data-in-modal="true">Auto-generated API key token</FormLabel>
</FormControl> </FormControl>
); );
}, []); }, []);
......
...@@ -12,7 +12,7 @@ type Props = { ...@@ -12,7 +12,7 @@ type Props = {
data?: ApiKey; data?: ApiKey;
} }
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const ApiKeyModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const title = data ? 'Edit API key' : 'New API key'; const title = data ? 'Edit API key' : 'New API key';
const text = !data ? 'Add an application name to identify your API key. Click the button below to auto-generate the associated key.' : ''; const text = !data ? 'Add an application name to identify your API key. Click the button below to auto-generate the associated key.' : '';
...@@ -34,4 +34,4 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -34,4 +34,4 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
); );
}; };
export default AddressModal; export default ApiKeyModal;
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import ContractVerificationForm from './ContractVerificationForm'; import ContractVerificationForm from './ContractVerificationForm';
...@@ -11,10 +13,49 @@ const hooksConfig = { ...@@ -11,10 +13,49 @@ const hooksConfig = {
}, },
}; };
const hash = '0x2F99338637F027CFB7494E46B49987457beCC6E3';
const formConfig: SmartContractVerificationConfig = {
solidity_compiler_versions: [
'v0.8.17+commit.8df45f5f',
'v0.8.16+commit.07a7930e',
'v0.8.15+commit.e14f2714',
'v0.8.18-nightly.2022.11.23+commit.eb2f874e',
'v0.8.17-nightly.2022.8.24+commit.22a0c46e',
'v0.8.16-nightly.2022.7.6+commit.b6f11b33',
],
solidity_evm_versions: [
'default',
'london',
'berlin',
],
verification_options: [
'flattened-code',
'standard-input',
'sourcify',
'multi-part',
'vyper-code',
],
vyper_compiler_versions: [
'v0.3.7+commit.6020b8bb',
'v0.3.1+commit.0463ea4c',
'v0.3.0+commit.8a23feb',
'v0.2.16+commit.59e1bdd',
'v0.2.3+commit.006968f',
'v0.2.2+commit.337c2ef',
'v0.1.0-beta.17+commit.0671b7b',
],
vyper_evm_versions: [
'byzantium',
'constantinople',
'petersburg',
'istanbul',
],
};
test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) => { test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -30,7 +71,7 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) = ...@@ -30,7 +71,7 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) =
test('standard input json method', async({ mount, page }) => { test('standard input json method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -43,7 +84,7 @@ test('standard input json method', async({ mount, page }) => { ...@@ -43,7 +84,7 @@ test('standard input json method', async({ mount, page }) => {
test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -62,7 +103,7 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -62,7 +103,7 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
test('multi-part files method', async({ mount, page }) => { test('multi-part files method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -75,7 +116,7 @@ test('multi-part files method', async({ mount, page }) => { ...@@ -75,7 +116,7 @@ test('multi-part files method', async({ mount, page }) => {
test('vyper contract method', async({ mount, page }) => { test('vyper contract method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
...@@ -4,54 +4,140 @@ import React from 'react'; ...@@ -4,54 +4,140 @@ import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { FormFields, VerificationMethod } from './types'; import type { FormFields } from './types';
import type { SocketMessage } from 'lib/socket/types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import delay from 'lib/delay'; import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast';
import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ContractVerificationFieldMethod, { VERIFICATION_METHODS } from './fields/ContractVerificationFieldMethod'; import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod';
import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode'; import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode';
import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile'; import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile';
import ContractVerificationSourcify from './methods/ContractVerificationSourcify'; import ContractVerificationSourcify from './methods/ContractVerificationSourcify';
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput'; import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract'; import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors } from './utils';
const METHODS = { const METHOD_COMPONENTS = {
flatten_source_code: <ContractVerificationFlattenSourceCode/>, 'flattened-code': <ContractVerificationFlattenSourceCode/>,
standard_input: <ContractVerificationStandardInput/>, 'standard-input': <ContractVerificationStandardInput/>,
sourcify: <ContractVerificationSourcify/>, sourcify: <ContractVerificationSourcify/>,
multi_part_file: <ContractVerificationMultiPartFile/>, 'multi-part': <ContractVerificationMultiPartFile/>,
vyper_contract: <ContractVerificationVyperContract/>, 'vyper-code': <ContractVerificationVyperContract/>,
'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
}; };
const ContractVerificationForm = () => { interface Props {
const router = useRouter(); method?: SmartContractVerificationMethod;
const methodFromQuery = router.query.method?.toString() as VerificationMethod; config: SmartContractVerificationConfig;
hash: string;
}
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: { defaultValues: {
method: VERIFICATION_METHODS.includes(methodFromQuery) ? methodFromQuery : undefined, method: methodFromQuery,
}, },
}); });
const { control, handleSubmit, watch, formState } = formApi; const { control, handleSubmit, watch, formState, setError } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const apiFetch = useApiFetch();
const toast = useToast();
const router = useRouter();
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
// eslint-disable-next-line no-console const body = prepareRequestBody(data);
console.log('__>__', data);
await delay(5_000);
}, []);
const method = watch('method'); try {
await apiFetch('contract_verification_via', {
pathParams: { method: data.method, id: hash },
fetchParams: {
method: 'POST',
body,
},
});
} catch (error) {
return;
}
return new Promise((resolve) => {
submitPromiseResolver.current = resolve;
});
}, [ apiFetch, hash ]);
const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback((payload) => {
if (payload.status === 'error') {
const errors = formatSocketErrors(payload.errors);
errors.forEach(([ field, error ]) => setError(field, error));
submitPromiseResolver.current?.(null);
return;
}
toast({
position: 'top-right',
title: 'Success',
description: 'Contract is successfully verified.',
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
router.push(link('address_index', { id: hash }, { tab: 'contract' }), undefined, { shallow: true });
},
});
}, [ hash, router, setError, toast ]);
const content = METHODS[method] || null; const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) {
return;
}
submitPromiseResolver.current?.(null);
const toastId = 'socket-error';
!toast.isActive(toastId) && toast({
id: toastId,
position: 'top-right',
title: 'Error',
description: 'There was an error with socket connection. Try again later.',
status: 'error',
variant: 'subtle',
isClosable: true,
});
}, [ formState.isSubmitting, toast ]);
const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: false,
});
useSocketMessage({
channel,
event: 'verification_result',
handler: handleNewSocketMessage,
});
const method = watch('method');
const content = METHOD_COMPONENTS[method] || null;
return ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
<chakra.form <chakra.form
noValidate noValidate
onSubmit={ handleSubmit(onFormSubmit) } onSubmit={ handleSubmit(onFormSubmit) }
mt={ 12 }
> >
<ContractVerificationFieldMethod control={ control } isDisabled={ Boolean(method) }/> <ContractVerificationFieldMethod
control={ control }
isDisabled={ Boolean(method) }
methods={ config.verification_options }
/>
{ content } { content }
{ Boolean(method) && ( { Boolean(method) && (
<Button <Button
......
...@@ -7,27 +7,39 @@ import type { FormFields } from '../types'; ...@@ -7,27 +7,39 @@ import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput'; import CheckboxInput from 'ui/shared/CheckboxInput';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationFieldConstructorArgs from './ContractVerificationFieldConstructorArgs';
const ContractVerificationFieldConstArgs = () => { const ContractVerificationFieldAutodetectArgs = () => {
const { formState, control } = useFormContext<FormFields>(); const [ isOn, setIsOn ] = React.useState(true);
const { formState, control, resetField } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'constructor_args'>}) => ( const handleCheckboxChange = React.useCallback(() => {
<CheckboxInput<FormFields, 'constructor_args'> !isOn && resetField('constructor_args');
setIsOn(prev => !prev);
}, [ isOn, resetField ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'autodetect_constructor_args'>}) => (
<CheckboxInput<FormFields, 'autodetect_constructor_args'>
text="Try to fetch constructor arguments automatically" text="Try to fetch constructor arguments automatically"
field={ field } field={ field }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
onChange={ handleCheckboxChange }
/> />
), [ formState.isSubmitting ]); ), [ formState.isSubmitting, handleCheckboxChange ]);
return ( return (
<ContractVerificationFormRow> <>
<Controller <ContractVerificationFormRow>
name="constructor_args" <Controller
control={ control } name="autodetect_constructor_args"
render={ renderControl } control={ control }
/> render={ renderControl }
</ContractVerificationFormRow> defaultValue={ true }
/>
</ContractVerificationFormRow>
{ !isOn && <ContractVerificationFieldConstructorArgs/> }
</>
); );
}; };
export default React.memo(ContractVerificationFieldConstArgs); export default React.memo(ContractVerificationFieldAutodetectArgs);
...@@ -5,6 +5,7 @@ import { useFormContext, Controller } from 'react-hook-form'; ...@@ -5,6 +5,7 @@ import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -27,7 +28,8 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => { ...@@ -27,7 +28,8 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => {
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
required required
/> />
<InputPlaceholder text="Contract code" error={ error }/> <InputPlaceholder text="Contract code"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl> </FormControl>
); );
}, [ formState.errors, formState.isSubmitting ]); }, [ formState.errors, formState.isSubmitting ]);
......
import { Code, Checkbox } from '@chakra-ui/react'; import { Code } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form'; import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect'; import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const COMPILERS = [ const OPTIONS_LIMIT = 50;
'v0.8.17+commit.8df45f5f',
'v0.8.16+commit.07a7930e',
'v0.8.15+commit.e14f2714',
];
const COMPILERS_NIGHTLY = [
'v0.8.18-nightly.2022.11.23+commit.eb2f874e',
'v0.8.17-nightly.2022.8.24+commit.22a0c46e',
'v0.8.16-nightly.2022.7.6+commit.b6f11b33',
];
interface Props { interface Props {
isVyper?: boolean; isVyper?: boolean;
} }
const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
const [ isNightly, setIsNightly ] = React.useState(false); const { formState, control } = useFormContext<FormFields>();
const { formState, control, getValues, resetField } = useFormContext<FormFields>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const options = React.useMemo(() => ( const options = React.useMemo(() => (
[ (isVyper ? config?.vyper_compiler_versions : config?.solidity_compiler_versions)?.map((option) => ({ label: option, value: option })) || []
...COMPILERS, ...(isNightly ? COMPILERS_NIGHTLY : []), ), [ config?.solidity_compiler_versions, config?.vyper_compiler_versions, isVyper ]);
].map((option) => ({ label: option, value: option }))
), [ isNightly ]); const loadOptions = React.useCallback(async(inputValue: string) => {
return options
.filter(({ label }) => {
if (!inputValue) {
return !label.toLowerCase().includes('nightly');
}
const handleCheckboxChange = React.useCallback(() => { return label.toLowerCase().includes(inputValue.toLowerCase());
if (isNightly) { })
const field = getValues('compiler'); .slice(0, OPTIONS_LIMIT);
field.value.includes('nightly') && resetField('compiler', { defaultValue: null }); }, [ options ]);
}
setIsNightly(prev => !prev);
}, [ getValues, isNightly, resetField ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => {
const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined; const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined;
...@@ -51,36 +47,26 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { ...@@ -51,36 +47,26 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
return ( return (
<FancySelect <FancySelect
{ ...field } { ...field }
options={ options } loadOptions={ loadOptions }
defaultOptions
size={ isMobile ? 'md' : 'lg' } size={ isMobile ? 'md' : 'lg' }
placeholder="Compiler" placeholder="Compiler"
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
error={ error } error={ error }
isRequired isRequired
isAsync
/> />
); );
}, [ formState.errors, formState.isSubmitting, isMobile, options ]); }, [ formState.errors, formState.isSubmitting, isMobile, loadOptions ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<> <Controller
<Controller name="compiler"
name="compiler" control={ control }
control={ control } render={ renderControl }
render={ renderControl } rules={{ required: true }}
rules={{ required: true }} />
/>
{ !isVyper && (
<Checkbox
size="lg"
mt={ 3 }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
>
Include nightly builds
</Checkbox>
) }
</>
{ isVyper ? null : ( { isVyper ? null : (
<> <>
<span>The compiler version is specified in </span> <span>The compiler version is specified in </span>
......
...@@ -5,32 +5,39 @@ import { Controller, useFormContext } from 'react-hook-form'; ...@@ -5,32 +5,39 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldAbiEncodedArgs = () => { const ContractVerificationFieldConstructorArgs = () => {
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'abi_encoded_args'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'constructor_args'>}) => {
const error = 'constructor_args' in formState.errors ? formState.errors.constructor_args : undefined;
return ( return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }}> <FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Textarea <Textarea
{ ...field } { ...field }
maxLength={ 255 } maxLength={ 255 }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
isInvalid={ Boolean(error) }
required
/> />
<InputPlaceholder text="ABI-encoded Constructor Arguments"/> <InputPlaceholder text="ABI-encoded Constructor Arguments"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl> </FormControl>
); );
}, [ formState.isSubmitting ]); }, [ formState.errors, formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <Controller
name="abi_encoded_args" name="constructor_args"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }}
/> />
<> <>
<span>Add arguments in </span> <span>Add arguments in </span>
...@@ -44,4 +51,4 @@ const ContractVerificationFieldAbiEncodedArgs = () => { ...@@ -44,4 +51,4 @@ const ContractVerificationFieldAbiEncodedArgs = () => {
); );
}; };
export default React.memo(ContractVerificationFieldAbiEncodedArgs); export default React.memo(ContractVerificationFieldConstructorArgs);
import { Link } from '@chakra-ui/react'; import { Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form'; import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect'; import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const VERSIONS = [ interface Props {
'default', isVyper?: boolean;
'london', }
'berlin',
];
const ContractVerificationFieldEvmVersion = () => { const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const options = React.useMemo(() => ( const options = React.useMemo(() => (
VERSIONS.map((option) => ({ label: option, value: option })) (isVyper ? config?.vyper_evm_versions : config?.solidity_evm_versions)?.map((option) => ({ label: option, value: option })) || []
), [ ]); ), [ config?.solidity_evm_versions, config?.vyper_evm_versions, isVyper ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'evm_version'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'evm_version'>}) => {
const error = 'evm_version' in formState.errors ? formState.errors.evm_version : undefined; const error = 'evm_version' in formState.errors ? formState.errors.evm_version : undefined;
......
...@@ -20,32 +20,26 @@ import React from 'react'; ...@@ -20,32 +20,26 @@ import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form'; import type { ControllerRenderProps, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import type { FormFields, VerificationMethod } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import infoIcon from 'icons/info.svg'; import infoIcon from 'icons/info.svg';
export const VERIFICATION_METHODS: Array<VerificationMethod> = [
'flatten_source_code',
'standard_input',
'sourcify',
'multi_part_file',
'vyper_contract',
];
interface Props { interface Props {
control: Control<FormFields>; control: Control<FormFields>;
isDisabled?: boolean; isDisabled?: boolean;
methods: SmartContractVerificationConfig['verification_options'];
} }
const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => { const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => {
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(); const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean();
const tooltipBg = useColorModeValue('gray.700', 'gray.900'); const tooltipBg = useColorModeValue('gray.700', 'gray.900');
const renderItem = React.useCallback((method: VerificationMethod) => { const renderItem = React.useCallback((method: SmartContractVerificationMethod) => {
switch (method) { switch (method) {
case 'flatten_source_code': case 'flattened-code':
return 'Via flattened source code'; return 'Via flattened source code';
case 'standard_input': case 'standard-input':
return ( return (
<> <>
<span>Via standard </span> <span>Via standard </span>
...@@ -91,10 +85,12 @@ const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => { ...@@ -91,10 +85,12 @@ const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => {
</Popover> </Popover>
</> </>
); );
case 'multi_part_file': case 'multi-part':
return 'Via multi-part files'; return 'Via multi-part files';
case 'vyper_contract': case 'vyper-code':
return 'Vyper contract'; return 'Vyper contract';
case 'vyper-multi-part':
return 'Via multi-part Vyper files';
default: default:
break; break;
...@@ -103,15 +99,15 @@ const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => { ...@@ -103,15 +99,15 @@ const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => {
const renderRadioGroup = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => { const renderRadioGroup = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
return ( return (
<RadioGroup defaultValue="add" colorScheme="blue" isDisabled={ isDisabled } { ...field }> <RadioGroup defaultValue="add" colorScheme="blue" isDisabled={ isDisabled } isFocusable={ !isDisabled } { ...field } >
<Stack spacing={ 4 }> <Stack spacing={ 4 }>
{ VERIFICATION_METHODS.map((method) => { { methods.map((method) => {
return <Radio key={ method } value={ method } size="lg">{ renderItem(method) }</Radio>; return <Radio key={ method } value={ method } size="lg">{ renderItem(method) }</Radio>;
}) } }) }
</Stack> </Stack>
</RadioGroup> </RadioGroup>
); );
}, [ isDisabled, renderItem ]); }, [ isDisabled, methods, renderItem ]);
return ( return (
<section> <section>
......
...@@ -11,7 +11,7 @@ import InputPlaceholder from 'ui/shared/InputPlaceholder'; ...@@ -11,7 +11,7 @@ import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldOptimization = () => { const ContractVerificationFieldOptimization = () => {
const [ isEnabled, setIsEnabled ] = React.useState(false); const [ isEnabled, setIsEnabled ] = React.useState(true);
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const handleCheckboxChange = React.useCallback(() => { const handleCheckboxChange = React.useCallback(() => {
...@@ -49,6 +49,7 @@ const ContractVerificationFieldOptimization = () => { ...@@ -49,6 +49,7 @@ const ContractVerificationFieldOptimization = () => {
name="is_optimization_enabled" name="is_optimization_enabled"
control={ control } control={ control }
render={ renderCheckboxControl } render={ renderCheckboxControl }
defaultValue={ true }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
{ isEnabled && ( { isEnabled && (
...@@ -58,7 +59,7 @@ const ContractVerificationFieldOptimization = () => { ...@@ -58,7 +59,7 @@ const ContractVerificationFieldOptimization = () => {
control={ control } control={ control }
render={ renderInputControl } render={ renderInputControl }
rules={{ required: true }} rules={{ required: true }}
defaultValue="" defaultValue="200"
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
) } ) }
......
import { Text, Button, Box, chakra } from '@chakra-ui/react'; import { Text, Button, Box, chakra, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { Mb } from 'lib/consts';
// import DragAndDropArea from 'ui/shared/forms/DragAndDropArea';
import FieldError from 'ui/shared/forms/FieldError';
import FileInput from 'ui/shared/forms/FileInput'; import FileInput from 'ui/shared/forms/FileInput';
import FileSnippet from 'ui/shared/forms/FileSnippet'; import FileSnippet from 'ui/shared/forms/FileSnippet';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
type FileTypes = '.sol' | '.yul' | '.json' | '.vy'
interface Props { interface Props {
accept?: string; fileTypes: Array<FileTypes>;
multiple?: boolean; multiple?: boolean;
title: string; title: string;
className?: string; className?: string;
hint: string; hint: string;
} }
const ContractVerificationFieldSources = ({ accept, multiple, title, className, hint }: Props) => { const ContractVerificationFieldSources = ({ fileTypes, multiple, title, className, hint }: Props) => {
const { setValue, getValues, control, formState } = useFormContext<FormFields>(); const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>();
const error = 'sources' in formState.errors ? formState.errors.sources : undefined; const error = 'sources' in formState.errors ? formState.errors.sources : undefined;
const commonError = !error?.type?.startsWith('file_') ? error : undefined;
const fileError = error?.type?.startsWith('file_') ? error : undefined;
const handleFileRemove = React.useCallback((index?: number) => { const handleFileRemove = React.useCallback((index?: number) => {
if (index === undefined) { if (index === undefined) {
...@@ -31,41 +38,89 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className, ...@@ -31,41 +38,89 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className,
const value = getValues('sources').slice(); const value = getValues('sources').slice();
value.splice(index, 1); value.splice(index, 1);
setValue('sources', value); setValue('sources', value);
clearErrors('sources');
}, [ getValues, setValue ]); }, [ getValues, clearErrors, setValue ]);
const renderFiles = React.useCallback((files: Array<File>) => { const renderFiles = React.useCallback((files: Array<File>) => {
const errorList = fileError?.message?.split(';');
return ( return (
<Box display="grid" gridTemplateColumns={{ base: '1fr', lg: '1fr 1fr' }} columnGap={ 3 } rowGap={ 3 }> <Box display="grid" gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }} columnGap={ 3 } rowGap={ 3 }>
{ files.map((file, index) => ( { files.map((file, index) => (
<FileSnippet <Box key={ file.name + file.lastModified + index }>
key={ file.name + file.lastModified } <FileSnippet
file={ file } file={ file }
maxW="initial" maxW="initial"
onRemove={ handleFileRemove } onRemove={ handleFileRemove }
index={ index } index={ index }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
/> />
{ errorList?.[index] && <FieldError message={ errorList?.[index] } mt={ 1 } px={ 3 }/> }
</Box>
)) } )) }
</Box> </Box>
); );
}, [ formState.isSubmitting, handleFileRemove ]); }, [ formState.isSubmitting, handleFileRemove, fileError ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => ( const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => (
<> <>
<FileInput<FormFields, 'sources'> accept={ accept } multiple={ multiple } field={ field }> <FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
<Button variant="outline" size="sm" display={ field.value && field.value.length > 0 ? 'none' : 'block' }> { () => (
Upload file{ multiple ? 's' : '' } <Flex
</Button> flexDir="column"
alignItems="flex-start"
rowGap={ 2 }
w="100%"
display={ field.value && field.value.length > 0 && !multiple ? 'none' : 'block' }
mb={ field.value && field.value.length > 0 ? 2 : 0 }
>
<Button
variant="outline"
size="sm"
// mb={ 2 }
>
Upload file{ multiple ? 's' : '' }
</Button>
{ /* design is not ready */ }
{ /* <DragAndDropArea onDrop={ onChange }/> */ }
</Flex>
) }
</FileInput> </FileInput>
{ field.value && field.value.length > 0 && renderFiles(field.value) } { field.value && field.value.length > 0 && renderFiles(field.value) }
{ error && ( { commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
<Box fontSize="sm" mt={ 2 } color="error">
{ error.type === 'required' ? 'Field is required' : error.message }
</Box>
) }
</> </>
), [ accept, error, multiple, renderFiles ]); ), [ fileTypes, commonError, multiple, renderFiles ]);
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (Array.isArray(value)) {
const errorText = `Wrong file type. Allowed files types are ${ fileTypes.join(',') }.`;
const errors = value.map(({ name }) => fileTypes.some((ext) => name.endsWith(ext)) ? '' : errorText);
if (errors.some((item) => item !== '')) {
return errors.join(';');
}
}
return true;
}, [ fileTypes ]);
const validateFileSize = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (Array.isArray(value)) {
const FILE_SIZE_LIMIT = 20 * Mb;
const errors = value.map(({ size }) => size > FILE_SIZE_LIMIT ? 'File is too big. Maximum size is 20 Mb.' : '');
if (errors.some((item) => item !== '')) {
return errors.join(';');
}
}
return true;
}, []);
const rules = React.useMemo(() => ({
required: true,
validate: {
file_type: validateFileType,
file_size: validateFileSize,
},
}), [ validateFileSize, validateFileType ]);
return ( return (
<> <>
...@@ -77,7 +132,7 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className, ...@@ -77,7 +132,7 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className,
name="sources" name="sources"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }} rules={ rules }
/> />
{ hint ? <span>{ hint }</span> : null } { hint ? <span>{ hint }</span> : null }
</ContractVerificationFormRow> </ContractVerificationFormRow>
......
import React from 'react'; import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode'; import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler'; import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstArgs from '../fields/ContractVerificationFieldConstArgs';
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion'; import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldIsYul from '../fields/ContractVerificationFieldIsYul'; import ContractVerificationFieldIsYul from '../fields/ContractVerificationFieldIsYul';
import ContractVerificationFieldLibraries from '../fields/ContractVerificationFieldLibraries'; import ContractVerificationFieldLibraries from '../fields/ContractVerificationFieldLibraries';
...@@ -19,7 +19,7 @@ const ContractVerificationFlattenSourceCode = () => { ...@@ -19,7 +19,7 @@ const ContractVerificationFlattenSourceCode = () => {
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
<ContractVerificationFieldCode/> <ContractVerificationFieldCode/>
<ContractVerificationFieldConstArgs/> <ContractVerificationFieldAutodetectArgs/>
<ContractVerificationFieldLibraries/> <ContractVerificationFieldLibraries/>
</ContractVerificationMethod> </ContractVerificationMethod>
); );
......
...@@ -7,6 +7,8 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi ...@@ -7,6 +7,8 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi
import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization'; import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources'; import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.sol' as const, '.yul' as const ];
const ContractVerificationMultiPartFile = () => { const ContractVerificationMultiPartFile = () => {
return ( return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification"> <ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification">
...@@ -14,7 +16,7 @@ const ContractVerificationMultiPartFile = () => { ...@@ -14,7 +16,7 @@ const ContractVerificationMultiPartFile = () => {
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
<ContractVerificationFieldSources <ContractVerificationFieldSources
accept=".sol,.yul" fileTypes={ FILE_TYPES }
multiple multiple
title="Sources *.sol or *.yul files" title="Sources *.sol or *.yul files"
hint="Upload all Solidity or Yul contract source files." hint="Upload all Solidity or Yul contract source files."
......
...@@ -3,11 +3,13 @@ import React from 'react'; ...@@ -3,11 +3,13 @@ import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources'; import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const, '.sol' as const ];
const ContractVerificationSourcify = () => { const ContractVerificationSourcify = () => {
return ( return (
<ContractVerificationMethod title="New Smart Contract Verification"> <ContractVerificationMethod title="New Smart Contract Verification">
<ContractVerificationFieldSources <ContractVerificationFieldSources
accept=".json" fileTypes={ FILE_TYPES }
multiple multiple
title="Sources and Metadata JSON" mt={ 0 } title="Sources and Metadata JSON" mt={ 0 }
hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation." hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation."
......
import React from 'react'; import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler'; import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstArgs from '../fields/ContractVerificationFieldConstArgs';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName'; import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources'; import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const ];
const ContractVerificationStandardInput = () => { const ContractVerificationStandardInput = () => {
return ( return (
<ContractVerificationMethod title="New Smart Contract Verification"> <ContractVerificationMethod title="New Smart Contract Verification">
<ContractVerificationFieldName/> <ContractVerificationFieldName/>
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldSources <ContractVerificationFieldSources
accept=".json" fileTypes={ FILE_TYPES }
title="Standard Input JSON" title="Standard Input JSON"
hint="Upload the standard input JSON file created during contract compilation." hint="Upload the standard input JSON file created during contract compilation."
/> />
<ContractVerificationFieldConstArgs/> <ContractVerificationFieldAutodetectArgs/>
</ContractVerificationMethod> </ContractVerificationMethod>
); );
}; };
......
import React from 'react'; import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAbiEncodedArgs from '../fields/ContractVerificationFieldAbiEncodedArgs';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode'; import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler'; import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstructorArgs from '../fields/ContractVerificationFieldConstructorArgs';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName'; import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
const ContractVerificationVyperContract = () => { const ContractVerificationVyperContract = () => {
...@@ -12,7 +12,7 @@ const ContractVerificationVyperContract = () => { ...@@ -12,7 +12,7 @@ const ContractVerificationVyperContract = () => {
<ContractVerificationFieldName hint="Must match the name specified in the code."/> <ContractVerificationFieldName hint="Must match the name specified in the code."/>
<ContractVerificationFieldCompiler isVyper/> <ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldCode isVyper/> <ContractVerificationFieldCode isVyper/>
<ContractVerificationFieldAbiEncodedArgs/> <ContractVerificationFieldConstructorArgs/>
</ContractVerificationMethod> </ContractVerificationMethod>
); );
}; };
......
import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.vy' as const ];
const ContractVerificationVyperMultiPartFile = () => {
return (
<ContractVerificationMethod title="New Vyper Smart Contract Verification">
<ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldEvmVersion isVyper/>
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
multiple
title="Sources *.vy files"
hint="Upload all Vyper contract source files."
/>
</ContractVerificationMethod>
);
};
export default React.memo(ContractVerificationVyperMultiPartFile);
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/FancySelect/types';
export type VerificationMethod = 'flatten_source_code' | 'standard_input' | 'sourcify' | 'multi_part_file' | 'vyper_contract'
export interface ContractLibrary { export interface ContractLibrary {
name: string; name: string;
address: string; address: string;
} }
export interface FormFieldsFlattenSourceCode { export interface FormFieldsFlattenSourceCode {
method: 'flatten_source_code'; method: 'flattened-code';
is_yul: boolean; is_yul: boolean;
name: string; name: string;
compiler: Option; compiler: Option;
...@@ -15,15 +13,18 @@ export interface FormFieldsFlattenSourceCode { ...@@ -15,15 +13,18 @@ export interface FormFieldsFlattenSourceCode {
is_optimization_enabled: boolean; is_optimization_enabled: boolean;
optimization_runs: string; optimization_runs: string;
code: string; code: string;
constructor_args: boolean; autodetect_constructor_args: boolean;
constructor_args: string;
libraries: Array<ContractLibrary>; libraries: Array<ContractLibrary>;
} }
export interface FormFieldsStandardInput { export interface FormFieldsStandardInput {
method: 'standard_input'; method: 'standard-input';
name: string; name: string;
compiler: Option; compiler: Option;
sources: Array<File>; sources: Array<File>;
autodetect_constructor_args: boolean;
constructor_args: string;
} }
export interface FormFieldsSourcify { export interface FormFieldsSourcify {
...@@ -32,21 +33,29 @@ export interface FormFieldsSourcify { ...@@ -32,21 +33,29 @@ export interface FormFieldsSourcify {
} }
export interface FormFieldsMultiPartFile { export interface FormFieldsMultiPartFile {
method: 'multi_part_file'; method: 'multi-part';
compiler: Option; compiler: Option;
evm_version: Option; evm_version: Option;
is_optimization_enabled: boolean; is_optimization_enabled: boolean;
optimization_runs: string; optimization_runs: string;
sources: Array<File>; sources: Array<File>;
libraries: Array<ContractLibrary>;
} }
export interface FormFieldsVyperContract { export interface FormFieldsVyperContract {
method: 'vyper_contract'; method: 'vyper-code';
name: string; name: string;
compiler: Option; compiler: Option;
code: string; code: string;
abi_encoded_args: string; constructor_args: string;
}
export interface FormFieldsVyperMultiPartFile {
method: 'vyper-multi-part';
compiler: Option;
evm_version: Option;
sources: Array<File>;
} }
export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify | export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify |
FormFieldsMultiPartFile | FormFieldsVyperContract; FormFieldsMultiPartFile | FormFieldsVyperContract | FormFieldsVyperMultiPartFile;
import type { FieldPath, ErrorOption } from 'react-hook-form';
import type { ContractLibrary, FormFields } from './types';
import type { SmartContractVerificationMethod, SmartContractVerificationError } from 'types/api/contract';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMethod> = [
'flattened-code',
'standard-input',
'sourcify',
'multi-part',
'vyper-code',
'vyper-multi-part',
];
export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod {
return method && SUPPORTED_VERIFICATION_METHODS.includes(method as SmartContractVerificationMethod) ? true : false;
}
export function sortVerificationMethods(methodA: SmartContractVerificationMethod, methodB: SmartContractVerificationMethod) {
const indexA = SUPPORTED_VERIFICATION_METHODS.indexOf(methodA);
const indexB = SUPPORTED_VERIFICATION_METHODS.indexOf(methodB);
if (indexA > indexB) {
return 1;
}
if (indexA < indexB) {
return -1;
}
return 0;
}
export function prepareRequestBody(data: FormFields): FetchParams['body'] {
switch (data.method) {
case 'flattened-code': {
return {
compiler_version: data.compiler?.value,
source_code: data.code,
is_optimization_enabled: data.is_optimization_enabled,
is_yul_contract: data.is_yul,
optimization_runs: data.optimization_runs,
contract_name: data.name,
libraries: reduceLibrariesArray(data.libraries),
evm_version: data.evm_version?.value,
autodetect_constructor_args: data.autodetect_constructor_args,
constructor_args: data.constructor_args,
};
}
case 'standard-input': {
const body = new FormData();
body.set('compiler_version', data.compiler?.value);
body.set('contract_name', data.name);
body.set('autodetect_constructor_args', String(Boolean(data.autodetect_constructor_args)));
body.set('constructor_args', data.constructor_args);
addFilesToFormData(body, data.sources);
return body;
}
case 'sourcify': {
const body = new FormData();
addFilesToFormData(body, data.sources);
return body;
}
case 'multi-part': {
const body = new FormData();
body.set('compiler_version', data.compiler?.value);
body.set('evm_version', data.evm_version?.value);
body.set('is_optimization_enabled', String(Boolean(data.is_optimization_enabled)));
data.is_optimization_enabled && body.set('optimization_runs', data.optimization_runs);
const libraries = reduceLibrariesArray(data.libraries);
libraries && body.set('libraries', JSON.stringify(libraries));
addFilesToFormData(body, data.sources);
return body;
}
case 'vyper-code': {
return {
compiler_version: data.compiler?.value,
source_code: data.code,
contract_name: data.name,
constructor_args: data.constructor_args,
};
}
case 'vyper-multi-part': {
const body = new FormData();
body.set('compiler_version', data.compiler?.value);
body.set('evm_version', data.evm_version?.value);
addFilesToFormData(body, data.sources);
return body;
}
default: {
return {};
}
}
}
function reduceLibrariesArray(libraries: Array<ContractLibrary> | undefined) {
if (!libraries || libraries.length === 0) {
return;
}
return libraries.reduce<Record<string, string>>((result, item) => {
result[item.name] = item.address;
return result;
}, {});
}
function addFilesToFormData(body: FormData, files: Array<File> | undefined) {
if (!files) {
return;
}
for (let index = 0; index < files.length; index++) {
const file = files[index];
body.set(`files[${ index }]`, file, file.name);
}
}
const API_ERROR_TO_FORM_FIELD: Record<keyof SmartContractVerificationError, FieldPath<FormFields>> = {
contract_source_code: 'code',
files: 'sources',
compiler_version: 'compiler',
constructor_arguments: 'constructor_args',
name: 'name',
};
export function formatSocketErrors(errors: SmartContractVerificationError): Array<[FieldPath<FormFields>, ErrorOption]> {
return Object.entries(errors).map(([ key, value ]) => {
return [ API_ERROR_TO_FORM_FIELD[key as keyof SmartContractVerificationError], { message: value.join(',') } ];
});
}
...@@ -2,7 +2,7 @@ import { Flex, Skeleton, Tag, Box, Icon } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Flex, Skeleton, Tag, Box, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import iconSuccess from 'icons/status/success.svg'; import iconSuccess from 'icons/status/success.svg';
......
...@@ -2,13 +2,19 @@ import { Text } from '@chakra-ui/react'; ...@@ -2,13 +2,19 @@ import { Text } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SmartContractVerificationConfigRaw, SmartContractVerificationMethod } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
import link from 'lib/link/link'; import link from 'lib/link/link';
import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm'; import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm';
import { isValidVerificationMethod, sortVerificationMethods } from 'ui/contractVerification/utils';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import ContentLoader from 'ui/shared/ContentLoader';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -21,7 +27,31 @@ const ContractVerification = () => { ...@@ -21,7 +27,31 @@ const ContractVerification = () => {
const router = useRouter(); const router = useRouter();
const hash = router.query.id?.toString(); const hash = router.query.id?.toString();
const method = router.query.id?.toString(); const method = router.query.id?.toString() as SmartContractVerificationMethod | undefined;
const contractQuery = useApiQuery('contract', {
pathParams: { id: hash },
queryOptions: {
enabled: Boolean(hash),
},
});
if (contractQuery.isError && contractQuery.error.status === 404) {
throw Error('Not found', { cause: contractQuery.error as unknown as Error });
}
const configQuery = useApiQuery('contract_verification_config', {
queryOptions: {
select: (data: unknown) => {
const _data = data as SmartContractVerificationConfigRaw;
return {
..._data,
verification_options: _data.verification_options.filter(isValidVerificationMethod).sort(sortVerificationMethods),
};
},
enabled: Boolean(hash),
},
});
React.useEffect(() => { React.useEffect(() => {
if (method && hash) { if (method && hash) {
...@@ -31,6 +61,32 @@ const ContractVerification = () => { ...@@ -31,6 +61,32 @@ const ContractVerification = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]); }, [ ]);
const isVerifiedContract = contractQuery.data?.is_verified;
React.useEffect(() => {
if (isVerifiedContract) {
router.push(link('address_index', { id: hash }, { tab: 'contract' }), undefined, { scroll: false, shallow: true });
}
}, [ hash, isVerifiedContract, router ]);
const content = (() => {
if (configQuery.isError || !hash || contractQuery.isError) {
return <DataFetchAlert/>;
}
if (configQuery.isLoading || contractQuery.isLoading || isVerifiedContract) {
return <ContentLoader/>;
}
return (
<ContractVerificationForm
method={ method && configQuery.data.verification_options.includes(method) ? method : undefined }
config={ configQuery.data }
hash={ hash }
/>
);
})();
return ( return (
<Page> <Page>
<PageTitle <PageTitle
...@@ -39,7 +95,7 @@ const ContractVerification = () => { ...@@ -39,7 +95,7 @@ const ContractVerification = () => {
backLinkLabel="Back to contract" backLinkLabel="Back to contract"
/> />
{ hash && ( { hash && (
<Address> <Address mb={ 12 }>
<AddressIcon address={{ hash, is_contract: true, implementation_name: null }} flexShrink={ 0 }/> <AddressIcon address={{ hash, is_contract: true, implementation_name: null }} flexShrink={ 0 }/>
<Text fontFamily="heading" ml={ 2 } fontWeight={ 500 } fontSize="lg" w={{ base: '100%', lg: 'auto' }} whiteSpace="nowrap" overflow="hidden"> <Text fontFamily="heading" ml={ 2 } fontWeight={ 500 } fontSize="lg" w={{ base: '100%', lg: 'auto' }} whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ hash }/> <HashStringShortenDynamic hash={ hash }/>
...@@ -47,7 +103,7 @@ const ContractVerification = () => { ...@@ -47,7 +103,7 @@ const ContractVerification = () => {
<CopyToClipboard text={ hash }/> <CopyToClipboard text={ hash }/>
</Address> </Address>
) } ) }
<ContractVerificationForm/> { content }
</Page> </Page>
); );
}; };
......
...@@ -16,13 +16,15 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -16,13 +16,15 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import TokenContractInfo from 'ui/token/TokenContractInfo'; import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails'; import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders'; import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenInventory from 'ui/token/TokenInventory';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
export type TokenTabs = 'token_transfers' | 'holders' export type TokenTabs = 'token_transfers' | 'holders' | 'inventory';
const TokenPageContent = () => { const TokenPageContent = () => {
const router = useRouter(); const router = useRouter();
...@@ -72,11 +74,24 @@ const TokenPageContent = () => { ...@@ -72,11 +74,24 @@ const TokenPageContent = () => {
}, },
}); });
const inventoryQuery = useQueryWithPages({
resourceName: 'token_inventory',
pathParams: { hash: router.query.hash?.toString() },
scrollRef,
options: {
enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && tokenQuery.data),
},
});
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> }, { id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> }, { id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
]; ];
if (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') {
tabs.push({ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> });
}
let hasPagination; let hasPagination;
let pagination; let pagination;
...@@ -90,6 +105,11 @@ const TokenPageContent = () => { ...@@ -90,6 +105,11 @@ const TokenPageContent = () => {
pagination = holdersQuery.pagination; pagination = holdersQuery.pagination;
} }
if (router.query.tab === 'inventory') {
hasPagination = inventoryQuery.isPaginationVisible;
pagination = inventoryQuery.pagination;
}
const tokenSymbolText = tokenQuery.data?.symbol ? ` (${ trimTokenSymbol(tokenQuery.data.symbol) })` : ''; const tokenSymbolText = tokenQuery.data?.symbol ? ` (${ trimTokenSymbol(tokenQuery.data.symbol) })` : '';
return ( return (
...@@ -118,12 +138,14 @@ const TokenPageContent = () => { ...@@ -118,12 +138,14 @@ const TokenPageContent = () => {
{ /* should stay before tabs to scroll up whith pagination */ } { /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
<RoutedTabs { tokenQuery.isLoading ? <SkeletonTabs/> : (
tabs={ tabs } <RoutedTabs
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } } tabs={ tabs }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null } tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
stickyEnabled={ !isMobile } rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
/> stickyEnabled={ !isMobile }
/>
) }
</Page> </Page>
); );
}; };
......
import React from 'react';
import Page from 'ui/shared/Page/Page';
import TokenInstanceContent from 'ui/tokenInstance/TokenInstanceContent';
export type TokenTabs = 'token_transfers' | 'holders'
const TokenInstance = () => {
return (
<Page>
<TokenInstanceContent/>
</Page>
);
};
export default TokenInstance;
...@@ -48,7 +48,7 @@ const TransactionPageContent = () => { ...@@ -48,7 +48,7 @@ const TransactionPageContent = () => {
}); });
const additionals = ( const additionals = (
<Flex justifyContent="space-between" alignItems="center" flexGrow={ 1 }> <Flex justifyContent="space-between" alignItems="center" flexGrow={ 1 } flexWrap="wrap">
{ data?.tx_tag && <Tag my={ 2 }>{ data.tx_tag }</Tag> } { data?.tx_tag && <Tag my={ 2 }>{ data.tx_tag }</Tag> }
{ explorersLinks.length > 0 && ( { explorersLinks.length > 0 && (
<Flex <Flex
......
import { Flex } from '@chakra-ui/react';
import React from 'react';
import type { AddressParam } from 'types/api/addressParams';
import type { TokenInfo } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names'>;
token?: TokenInfo | null;
isLinkDisabled?: boolean;
}
const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => {
const isMobile = useIsMobile();
return (
<Flex alignItems="center">
<AddressIcon address={ address }/>
<AddressLink
type="address"
hash={ address.hash }
ml={ 2 }
fontFamily="heading"
fontWeight={ 500 }
truncation={ isMobile ? 'constant' : 'none' }
isDisabled={ isLinkDisabled }
/>
<CopyToClipboard text={ address.hash }/>
{ address.is_contract && token && <AddressAddToMetaMask ml={ 2 } token={ token }/> }
{ !address.is_contract && (
<AddressFavoriteButton hash={ address.hash } isAdded={ Boolean(address.watchlist_names?.length) } ml={ 3 }/>
) }
<AddressQrCode hash={ address.hash } ml={ 2 }/>
</Flex>
);
};
export default AddressHeadingInfo;
...@@ -8,13 +8,14 @@ interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> { ...@@ -8,13 +8,14 @@ interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
title: React.ReactNode; title: React.ReactNode;
hint: string; hint: string;
children: React.ReactNode; children: React.ReactNode;
note?: string;
} }
const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => { const DetailsInfoItem = ({ title, hint, note, children, id, ...styles }: Props) => {
return ( return (
<> <>
<GridItem py={{ base: 1, lg: 2 }} lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}> <GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}>
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="flex-start">
<Tooltip <Tooltip
label={ hint } label={ hint }
placement="top" placement="top"
...@@ -24,7 +25,10 @@ const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => { ...@@ -24,7 +25,10 @@ const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => {
<Icon as={ infoIcon } boxSize={ 5 }/> <Icon as={ infoIcon } boxSize={ 5 }/>
</Box> </Box>
</Tooltip> </Tooltip>
<Text fontWeight={{ base: 700, lg: 500 }}>{ title }</Text> <Text fontWeight={{ base: 700, lg: 500 }}>
{ title }
{ note && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">{ note }</Text> }
</Text>
</Flex> </Flex>
</GridItem> </GridItem>
<GridItem <GridItem
......
import { FormControl, useToken, useColorMode } from '@chakra-ui/react'; import { FormControl, useToken, useColorMode } from '@chakra-ui/react';
import type { Size, CSSObjectWithLabel, OptionsOrGroups, GroupBase, SingleValue, MultiValue } from 'chakra-react-select'; import type { CSSObjectWithLabel, GroupBase, SingleValue, MultiValue, AsyncProps, Props as SelectProps } from 'chakra-react-select';
import { Select } from 'chakra-react-select'; import { Select, AsyncSelect } from 'chakra-react-select';
import React from 'react'; import React from 'react';
import type { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form'; import type { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form';
...@@ -9,20 +9,23 @@ import type { Option } from './types'; ...@@ -9,20 +9,23 @@ import type { Option } from './types';
import { getChakraStyles } from 'ui/shared/FancySelect/utils'; import { getChakraStyles } from 'ui/shared/FancySelect/utils';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props { interface CommonProps {
size?: Size; // we don't have styles for sm select/input with floating label yet
options: OptionsOrGroups<Option, GroupBase<Option>>;
placeholder: string;
name: string;
onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void;
onBlur?: () => void;
isDisabled?: boolean;
isRequired?: boolean;
error?: Merge<FieldError, FieldErrorsImpl<Option>> | undefined; error?: Merge<FieldError, FieldErrorsImpl<Option>> | undefined;
value?: SingleValue<Option> | MultiValue<Option>;
} }
const FancySelect = ({ size = 'md', options, placeholder, name, onChange, onBlur, isDisabled, isRequired, error, value }: Props) => { interface RegularSelectProps extends SelectProps<Option, boolean, GroupBase<Option>>, CommonProps {
isAsync?: false;
onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void;
}
interface AsyncSelectProps extends AsyncProps<Option, boolean, GroupBase<Option>>, CommonProps {
isAsync: true;
onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void;
}
type Props = RegularSelectProps | AsyncSelectProps;
const FancySelect = (props: Props) => {
const menuZIndex = useToken('zIndices', 'dropdown'); const menuZIndex = useToken('zIndices', 'dropdown');
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
...@@ -32,34 +35,30 @@ const FancySelect = ({ size = 'md', options, placeholder, name, onChange, onBlur ...@@ -32,34 +35,30 @@ const FancySelect = ({ size = 'md', options, placeholder, name, onChange, onBlur
const chakraStyles = React.useMemo(() => getChakraStyles(colorMode), [ colorMode ]); const chakraStyles = React.useMemo(() => getChakraStyles(colorMode), [ colorMode ]);
const SelectComponent = props.isAsync ? AsyncSelect : Select;
return ( return (
<FormControl <FormControl
variant="floating" variant="floating"
size={ size } size={ props.size || 'md' }
isRequired={ isRequired } isRequired={ props.isRequired }
{ ...(error ? { 'aria-invalid': true } : {}) } { ...(props.error ? { 'aria-invalid': true } : {}) }
{ ...(isDisabled ? { 'aria-disabled': true } : {}) } { ...(props.isDisabled ? { 'aria-disabled': true } : {}) }
{ ...(value ? { 'data-active': true } : {}) } { ...(props.value ? { 'data-active': true } : {}) }
> >
<Select <SelectComponent
{ ...props }
size={ props.size || 'md' }
menuPortalTarget={ window.document.body } menuPortalTarget={ window.document.body }
placeholder="" placeholder=""
name={ name }
options={ options }
size={ size }
styles={ styles } styles={ styles }
chakraStyles={ chakraStyles } chakraStyles={ chakraStyles }
isInvalid={ Boolean(props.error) }
useBasicStyles useBasicStyles
onChange={ onChange }
onBlur={ onBlur }
isDisabled={ isDisabled }
isRequired={ isRequired }
isInvalid={ Boolean(error) }
value={ value }
/> />
<InputPlaceholder <InputPlaceholder
text={ placeholder } text={ typeof props.placeholder === 'string' ? props.placeholder : '' }
error={ error } error={ props.error }
isFancy isFancy
/> />
</FormControl> </FormControl>
......
...@@ -7,9 +7,10 @@ interface Props { ...@@ -7,9 +7,10 @@ interface Props {
error?: Partial<FieldError>; error?: Partial<FieldError>;
className?: string; className?: string;
isFancy?: boolean; isFancy?: boolean;
isInModal?: boolean;
} }
const InputPlaceholder = ({ text, error, className, isFancy }: Props) => { const InputPlaceholder = ({ text, error, className, isFancy, isInModal }: Props) => {
let errorMessage = error?.message; let errorMessage = error?.message;
if (!errorMessage && error?.type === 'pattern') { if (!errorMessage && error?.type === 'pattern') {
...@@ -20,6 +21,7 @@ const InputPlaceholder = ({ text, error, className, isFancy }: Props) => { ...@@ -20,6 +21,7 @@ const InputPlaceholder = ({ text, error, className, isFancy }: Props) => {
<FormLabel <FormLabel
className={ className } className={ className }
{ ...(isFancy ? { 'data-fancy': true } : {}) } { ...(isFancy ? { 'data-fancy': true } : {}) }
{ ...(isInModal ? { 'data-in-modal': true } : {}) }
> >
<chakra.span>{ text }</chakra.span> <chakra.span>{ text }</chakra.span>
{ errorMessage && <chakra.span order={ 3 } whiteSpace="pre"> - { errorMessage }</chakra.span> } { errorMessage && <chakra.span order={ 3 } whiteSpace="pre"> - { errorMessage }</chakra.span> }
......
...@@ -35,7 +35,7 @@ const Page = ({ ...@@ -35,7 +35,7 @@ const Page = ({
return <PageContent isHomePage={ isHomePage }>{ content }</PageContent>; return <PageContent isHomePage={ isHomePage }>{ content }</PageContent>;
} }
return isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ 500 }/>; return isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode }/>;
}, [ isHomePage, wrapChildren ]); }, [ isHomePage, wrapChildren ]);
const renderedChildren = wrapChildren ? ( const renderedChildren = wrapChildren ? (
......
...@@ -21,6 +21,7 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi ...@@ -21,6 +21,7 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi
as="h1" as="h1"
size="lg" size="lg"
flex="none" flex="none"
wordBreak="break-word"
> >
{ text } { text }
</Heading> </Heading>
......
...@@ -10,13 +10,14 @@ interface Props { ...@@ -10,13 +10,14 @@ interface Props {
name?: string | null; name?: string | null;
className?: string; className?: string;
logoSize?: number; logoSize?: number;
isDisabled?: boolean;
} }
const TokenSnippet = ({ symbol, hash, name, className, logoSize = 6 }: Props) => { const TokenSnippet = ({ symbol, hash, name, className, logoSize = 6, isDisabled }: Props) => {
return ( return (
<Flex className={ className } alignItems="center" columnGap={ 2 } w="100%"> <Flex className={ className } alignItems="center" columnGap={ 2 } w="100%">
<TokenLogo boxSize={ logoSize } hash={ hash } name={ name }/> <TokenLogo boxSize={ logoSize } hash={ hash } name={ name }/>
<AddressLink hash={ hash } alias={ name } type="token"/> <AddressLink hash={ hash } alias={ name } type="token" isDisabled={ isDisabled }/>
{ symbol && <Text variant="secondary">({ symbol })</Text> } { symbol && <Text variant="secondary">({ symbol })</Text> }
</Flex> </Flex>
); );
......
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
import React from 'react'; import React from 'react';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import PopoverFilter from 'ui/shared/filters/PopoverFilter'; import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter'; import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
......
import { CheckboxGroup, Checkbox, Text } from '@chakra-ui/react'; import { CheckboxGroup, Checkbox, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import TOKEN_TYPE from 'lib/token/tokenTypes'; import TOKEN_TYPE from 'lib/token/tokenTypes';
......
import { Box } from '@chakra-ui/react';
import type { DragEvent } from 'react';
import React from 'react';
import { getAllFileEntries, convertFileEntryToFile } from './utils/files';
interface Props {
onDrop: (files: Array<File>) => void;
}
const DragAndDropArea = ({ onDrop }: Props) => {
const [ isDragOver, setIsDragOver ] = React.useState(false);
const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
const fileEntries = await getAllFileEntries(event.dataTransfer.items);
const files = await Promise.all(fileEntries.map(convertFileEntryToFile));
onDrop(files);
setIsDragOver(false);
}, [ onDrop ]);
const handleDragOver = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
}, []);
const handleDragEnter = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(true);
}, []);
const handleDragLeave = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(false);
}, []);
return (
<Box
w="100%"
h="200px"
bgColor="lightpink"
opacity={ isDragOver ? 0.8 : 1 }
onDrop={ handleDrop }
onDragOver={ handleDragOver }
onDragEnter={ handleDragEnter }
onDragLeave={ handleDragLeave }
/>
);
};
export default React.memo(DragAndDropArea);
import { Box, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
message: string;
className?: string;
}
const FieldError = ({ message, className }: Props) => {
return <Box className={ className } color="error" fontSize="sm" mt={ 2 } wordBreak="break-all">{ message }</Box>;
};
export default chakra(FieldError);
...@@ -3,8 +3,12 @@ import type { ChangeEvent } from 'react'; ...@@ -3,8 +3,12 @@ import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, FieldValues, Path } from 'react-hook-form'; import type { ControllerRenderProps, FieldValues, Path } from 'react-hook-form';
interface InjectedProps {
onChange: (files: Array<File>) => void;
}
interface Props<V extends FieldValues, N extends Path<V>> { interface Props<V extends FieldValues, N extends Path<V>> {
children: React.ReactNode; children: React.ReactNode | ((props: InjectedProps) => React.ReactNode);
field: ControllerRenderProps<V, N>; field: ControllerRenderProps<V, N>;
accept?: string; accept?: string;
multiple?: boolean; multiple?: boolean;
...@@ -13,6 +17,10 @@ interface Props<V extends FieldValues, N extends Path<V>> { ...@@ -13,6 +17,10 @@ interface Props<V extends FieldValues, N extends Path<V>> {
const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ children, accept, multiple, field }: Props<Values, Names>) => { const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ children, accept, multiple, field }: Props<Values, Names>) => {
const ref = React.useRef<HTMLInputElement>(null); const ref = React.useRef<HTMLInputElement>(null);
const onChange = React.useCallback((files: Array<File>) => {
field.onChange([ ...(field.value || []), ...files ]);
}, [ field ]);
const handleInputChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleInputChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
const fileList = event.target.files; const fileList = event.target.files;
if (!fileList) { if (!fileList) {
...@@ -20,9 +28,9 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi ...@@ -20,9 +28,9 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi
} }
const files = Array.from(fileList); const files = Array.from(fileList);
field.onChange(files); onChange(files);
field.onBlur(); field.onBlur();
}, [ field ]); }, [ onChange, field ]);
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
ref.current?.click(); ref.current?.click();
...@@ -32,6 +40,12 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi ...@@ -32,6 +40,12 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi
field.onBlur(); field.onBlur();
}, [ field ]); }, [ field ]);
const injectedProps = React.useMemo(() => ({
onChange,
}), [ onChange ]);
const content = typeof children === 'function' ? children(injectedProps) : children;
return ( return (
<InputGroup onClick={ handleClick } onBlur={ handleInputBlur }> <InputGroup onClick={ handleClick } onBlur={ handleInputBlur }>
<VisuallyHiddenInput <VisuallyHiddenInput
...@@ -42,7 +56,7 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi ...@@ -42,7 +56,7 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi
multiple={ multiple } multiple={ multiple }
name={ field.name } name={ field.name }
/> />
{ children } { content }
</InputGroup> </InputGroup>
); );
}; };
......
// Function to get all files in drop directory
export async function getAllFileEntries(dataTransferItemList: DataTransferItemList): Promise<Array<FileSystemFileEntry>> {
const fileEntries: Array<FileSystemFileEntry> = [];
// Use BFS to traverse entire directory/file structure
const queue: Array<FileSystemFileEntry | FileSystemDirectoryEntry> = [];
// Unfortunately dataTransferItemList is not iterable i.e. no forEach
for (let i = 0; i < dataTransferItemList.length; i++) {
// Note webkitGetAsEntry a non-standard feature and may change
// Usage is necessary for handling directories
// + typescript types are kinda wrong - https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
const item = dataTransferItemList[i].webkitGetAsEntry() as FileSystemFileEntry | FileSystemDirectoryEntry | null;
item && queue.push(item);
}
while (queue.length > 0) {
const entry = queue.shift();
if (entry?.isFile) {
fileEntries.push(entry as FileSystemFileEntry);
} else if (entry?.isDirectory && 'createReader' in entry) {
queue.push(...await readAllDirectoryEntries(entry.createReader()));
}
}
return fileEntries;
}
// Get all the entries (files or sub-directories) in a directory
// by calling readEntries until it returns empty array
async function readAllDirectoryEntries(directoryReader: DirectoryReader) {
const entries: Array<FileSystemFileEntry> = [];
let readEntries = await readEntriesPromise(directoryReader);
while (readEntries && readEntries.length > 0) {
entries.push(...readEntries);
readEntries = await readEntriesPromise(directoryReader);
}
return entries;
}
// Wrap readEntries in a promise to make working with readEntries easier
// readEntries will return only some of the entries in a directory
// e.g. Chrome returns at most 100 entries at a time
async function readEntriesPromise(directoryReader: DirectoryReader): Promise<Array<FileSystemFileEntry> | undefined> {
try {
return await new Promise((resolve, reject) => {
directoryReader.readEntries(
(fileEntry) => {
resolve(fileEntry as Array<FileSystemFileEntry>);
},
reject,
);
});
} catch (err) {}
}
export function convertFileEntryToFile(entry: FileSystemFileEntry): Promise<File> {
return new Promise((resolve) => {
entry.file(async(file: File) => {
// const newFile = new File([ file ], entry.fullPath, { lastModified: file.lastModified, type: file.type });
resolve(file);
});
});
}
import type { ResponsiveValue } from '@chakra-ui/react';
import { AspectRatio, chakra, Icon, Image, shouldForwardProp, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { Property } from 'csstype';
import React from 'react';
import nftIcon from 'icons/nft_shield.svg';
interface Props {
url: string | null;
className?: string;
fallbackPadding?: string;
objectFit: ResponsiveValue<Property.ObjectFit>;
}
interface FallbackProps {
className?: string;
padding?: string;
}
const Fallback = ({ className, padding }: FallbackProps) => {
return (
<Icon
className={ className }
as={ nftIcon }
p={ padding ?? '50px' }
color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') }
/>
);
};
const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
const [ isLoading, setIsLoading ] = React.useState(true);
const handleLoad = React.useCallback(() => {
setIsLoading(false);
}, []);
const handleLoadError = React.useCallback(() => {
setIsLoading(false);
}, []);
const _objectFit = objectFit || 'contain';
return (
<AspectRatio
className={ className }
ratio={ 1 / 1 }
bgColor={ useColorModeValue('blackAlpha.50', 'whiteAlpha.50') }
overflow="hidden"
borderRadius="md"
sx={{
'&>img': {
objectFit: _objectFit,
},
}}
>
<Image
w="100%"
h="100%"
objectFit={ _objectFit }
src={ url || undefined }
alt="Token instance image"
fallback={ url && isLoading ? <Skeleton/> : <Fallback className={ className } padding={ fallbackPadding }/> }
onError={ handleLoadError }
onLoad={ handleLoad }
/>
</AspectRatio>
);
};
const NftImageChakra = chakra(NftImage, {
shouldForwardProp: (prop) => {
const isChakraProp = !shouldForwardProp(prop);
if (isChakraProp && prop !== 'objectFit') {
return false;
}
return true;
},
});
export default NftImageChakra;
import { AspectRatio, chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import NftImage from './NftImage';
import NftVideo from './NftVideo';
interface Props {
imageUrl: string | null;
animationUrl: string | null;
className?: string;
}
const NftMedia = ({ imageUrl, animationUrl, className }: Props) => {
const [ type, setType ] = React.useState<'image' | 'video' | undefined>(!animationUrl ? 'image' : undefined);
React.useEffect(() => {
if (!animationUrl) {
return;
}
// media could be either gif or video
// so we pre-fetch the resources in order to get its content type
fetch(animationUrl, { method: 'HEAD' })
.then((response) => {
const contentType = response.headers.get('content-type');
setType(contentType?.startsWith('video') ? 'video' : 'image');
});
}, [ animationUrl ]);
if (!type) {
return (
<AspectRatio
className={ className }
ratio={ 1 / 1 }
overflow="hidden"
borderRadius="md"
>
<Skeleton/>
</AspectRatio>
);
}
if (animationUrl && type === 'video') {
return <NftVideo className={ className } src={ animationUrl }/>;
}
return <NftImage className={ className } url={ animationUrl || imageUrl }/>;
};
export default chakra(NftMedia);
import { AspectRatio, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
className?: string;
src: string;
}
const NftVideo = ({ className, src }: Props) => {
const [ isLoading, setIsLoading ] = React.useState(true);
const handleCanPlay = React.useCallback(() => {
setIsLoading(false);
}, []);
return (
<AspectRatio
className={ className }
ratio={ 1 / 1 }
overflow="hidden"
borderRadius="md"
>
<>
<chakra.video
src={ src }
autoPlay
disablePictureInPicture
loop
muted
playsInline
onCanPlayThrough={ handleCanPlay }
borderRadius="md"
/>
{ isLoading && <Skeleton position="absolute" w="100%" h="100%" left={ 0 } top={ 0 }/> }
</>
</AspectRatio>
);
};
export default chakra(NftVideo);
import { GridItem, Skeleton, SkeletonCircle } from '@chakra-ui/react'; import { GridItem, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
const DetailsSkeletonRow = ({ w = '100%' }: { w?: string }) => { const DetailsSkeletonRow = ({ w = '100%', maxW }: { w?: string; maxW?: string }) => {
return ( return (
<> <>
<GridItem display="flex" columnGap={ 2 } w={{ base: '50%', lg: 'auto' }} _notFirst={{ mt: { base: 3, lg: 0 } }}> <GridItem display="flex" columnGap={ 2 } w={{ base: '50%', lg: 'auto' }} _notFirst={{ mt: { base: 3, lg: 0 } }}>
...@@ -9,7 +9,7 @@ const DetailsSkeletonRow = ({ w = '100%' }: { w?: string }) => { ...@@ -9,7 +9,7 @@ const DetailsSkeletonRow = ({ w = '100%' }: { w?: string }) => {
<Skeleton flexGrow={ 1 } h={ 5 } borderRadius="full"/> <Skeleton flexGrow={ 1 } h={ 5 } borderRadius="full"/>
</GridItem> </GridItem>
<GridItem pl={{ base: 7, lg: 0 }}> <GridItem pl={{ base: 7, lg: 0 }}>
<Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }}/> <Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }} maxW={ maxW }/>
</GridItem> </GridItem>
</> </>
); );
......
...@@ -3,15 +3,10 @@ import type { UseQueryResult } from '@tanstack/react-query'; ...@@ -3,15 +3,10 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressContractIcon from 'ui/shared/address/AddressContractIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
tokenQuery: UseQueryResult<TokenInfo>; tokenQuery: UseQueryResult<TokenInfo>;
...@@ -19,7 +14,6 @@ interface Props { ...@@ -19,7 +14,6 @@ interface Props {
const TokenContractInfo = ({ tokenQuery }: Props) => { const TokenContractInfo = ({ tokenQuery }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile();
const contractQuery = useApiQuery('address', { const contractQuery = useApiQuery('address', {
pathParams: { id: router.query.hash?.toString() }, pathParams: { id: router.query.hash?.toString() },
...@@ -43,17 +37,14 @@ const TokenContractInfo = ({ tokenQuery }: Props) => { ...@@ -43,17 +37,14 @@ const TokenContractInfo = ({ tokenQuery }: Props) => {
return null; return null;
} }
const hash = tokenQuery.data.address; const address = {
hash: tokenQuery.data.address,
return ( is_contract: true,
<Flex alignItems="center"> implementation_name: null,
<AddressContractIcon/> watchlist_names: [],
<AddressLink type="address" hash={ hash } ml={ 2 } truncation={ isMobile ? 'constant' : 'none' }/> };
<CopyToClipboard text={ hash } ml={ 1 }/>
{ contractQuery.data?.token && <AddressAddToMetaMask token={ contractQuery.data?.token } ml={ 2 }/> } return <AddressHeadingInfo address={ address } token={ contractQuery.data?.token }/>;
<AddressQrCode hash={ hash } ml={ 2 }/>
</Flex>
);
}; };
export default React.memo(TokenContractInfo); export default React.memo(TokenContractInfo);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
File mode changed from 100644 to 100755
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment