diff --git a/.changeset/lucky-jokes-change.md b/.changeset/lucky-jokes-change.md
new file mode 100644
index 000000000..ec7d16fad
--- /dev/null
+++ b/.changeset/lucky-jokes-change.md
@@ -0,0 +1,5 @@
+---
+"@exactly/server": patch
+---
+
+✨ use gcp kms for allower
diff --git a/.changeset/silly-yaks-divide.md b/.changeset/silly-yaks-divide.md
new file mode 100644
index 000000000..d3f7700c5
--- /dev/null
+++ b/.changeset/silly-yaks-divide.md
@@ -0,0 +1,5 @@
+---
+"@exactly/server": patch
+---
+
+✨ poke account after kyc
diff --git a/.do/app.yaml b/.do/app.yaml
index a82ea1324..518d88eff 100644
--- a/.do/app.yaml
+++ b/.do/app.yaml
@@ -84,6 +84,19 @@ services:
- key: DEBUG
scope: RUN_TIME
value: ${{ env.DEBUG }}
+ - key: GCP_KMS_KEY_RING
+ scope: RUN_TIME
+ value: ${{ env.GCP_KMS_KEY_RING }}
+ - key: GCP_KMS_KEY_VERSION
+ scope: RUN_TIME
+ value: ${{ env.GCP_KMS_KEY_VERSION }}
+ - key: GCP_PROJECT_ID
+ scope: RUN_TIME
+ value: ${{ env.GCP_PROJECT_ID }}
+ - key: GCP_BASE64_JSON
+ scope: RUN_TIME
+ type: SECRET
+ value: ${{ env.ENCRYPTED_GCP_BASE64_JSON || env.GCP_BASE64_JSON }}
- key: INTERCOM_IDENTITY_KEY
scope: RUN_TIME
type: SECRET
diff --git a/cspell.json b/cspell.json
index 6877550ca..e9c475888 100644
--- a/cspell.json
+++ b/cspell.json
@@ -175,6 +175,7 @@
"valibot",
"valierror",
"valkey",
+ "valora",
"viem",
"viewability",
"wagmi",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 25a0ff669..fbc118f17 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -733,6 +733,9 @@ importers:
'@exactly/lib':
specifier: ^0.1.0
version: 0.1.0
+ '@google-cloud/kms':
+ specifier: ^5.3.0
+ version: 5.4.0
'@hono/node-server':
specifier: ^1.19.10
version: 1.19.10(hono@4.12.7)
@@ -769,6 +772,9 @@ importers:
'@valibot/to-json-schema':
specifier: ^1.5.0
version: 1.5.0(valibot@1.2.0(typescript@5.9.3))
+ '@valora/viem-account-hsm-gcp':
+ specifier: ^1.2.16
+ version: 1.2.17(viem@2.46.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.5))
async-mutex:
specifier: ^0.5.0
version: 0.5.0
@@ -3147,6 +3153,19 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+ '@google-cloud/kms@5.4.0':
+ resolution: {integrity: sha512-+06zUCaJM+wyZISM3F6u/jSqoBs0iZ8Aj9rqOJFePoWkNN7FbR4mQpV7okGHA+Y7caVgq+4QtIDKiFd17SZT+A==}
+ engines: {node: '>=18'}
+
+ '@grpc/grpc-js@1.14.3':
+ resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==}
+ engines: {node: '>=12.10.0'}
+
+ '@grpc/proto-loader@0.8.0':
+ resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==}
+ engines: {node: '>=6'}
+ hasBin: true
+
'@hapi/address@5.1.1':
resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==}
engines: {node: '>=14.0.0'}
@@ -3574,6 +3593,9 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@js-sdsl/ordered-map@4.4.2':
+ resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
+
'@jsdevtools/ono@7.1.3':
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
@@ -4107,6 +4129,10 @@ packages:
'@pix.js/validator@1.1.0':
resolution: {integrity: sha512-NIYcYwuFblA8/cx7YpNdEEujNjKsnA985jsNgIMcYtY2AVUz646IUbisgTgFu7erN7X5eeQGzELgRnFoPcInVw==}
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
'@pkgr/core@0.1.2':
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@@ -4135,6 +4161,36 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.8
+ '@protobufjs/aspromise@1.1.2':
+ resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
+
+ '@protobufjs/base64@1.1.2':
+ resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
+
+ '@protobufjs/codegen@2.0.4':
+ resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
+
+ '@protobufjs/eventemitter@1.1.0':
+ resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
+
+ '@protobufjs/fetch@1.1.0':
+ resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==}
+
+ '@protobufjs/float@1.0.2':
+ resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
+
+ '@protobufjs/inquire@1.1.0':
+ resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
+
+ '@protobufjs/path@1.1.2':
+ resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
+
+ '@protobufjs/pool@1.1.0':
+ resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
+
+ '@protobufjs/utf8@1.1.0':
+ resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
+
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
@@ -5756,6 +5812,10 @@ packages:
'@tanstack/store@0.8.0':
resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==}
+ '@tootallnate/once@2.0.0':
+ resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
+ engines: {node: '>= 10'}
+
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -6218,6 +6278,12 @@ packages:
peerDependencies:
valibot: ^1.2.0
+ '@valora/viem-account-hsm-gcp@1.2.17':
+ resolution: {integrity: sha512-xRQ6C9qIFqQi6JQYGFenQDeiK39RXLbEG+4/uiHqGQqsrTiELm+fOkjhbX4vC+E009zvb340GuSP5QPVPPmOIw==}
+ engines: {node: '>=20'}
+ peerDependencies:
+ viem: ^2.9.20
+
'@vitest/coverage-v8@4.0.17':
resolution: {integrity: sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==}
peerDependencies:
@@ -6784,6 +6850,9 @@ packages:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
+ bignumber.js@9.3.1:
+ resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
+
birecord@0.1.1:
resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==}
@@ -6855,6 +6924,9 @@ packages:
resolution: {integrity: sha512-Rqf0ly5H4HGt+ki/n3m7GxoR2uIGtNqezPlOLX8Vuo13j5/tfPuVvAr84eoGF7sYm6lKdbGnT/3q8qmzuT5Y9w==}
engines: {node: '>= 0.4.0'}
+ buffer-equal-constant-time@1.0.1:
+ resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
+
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -7554,6 +7626,10 @@ packages:
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
+ data-uri-to-buffer@4.0.1:
+ resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
+ engines: {node: '>= 12'}
+
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -7875,9 +7951,15 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
+ duplexify@4.1.3:
+ resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
+
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ ecdsa-sig-formatter@1.0.11:
+ resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
+
edit-json-file@1.8.1:
resolution: {integrity: sha512-x8L381+GwqxQejPipwrUZIyAg5gDQ9tLVwiETOspgXiaQztLsrOm7luBW5+Pe31aNezuzDY79YyzF+7viCRPXA==}
@@ -8769,6 +8851,10 @@ packages:
fengari@0.1.5:
resolution: {integrity: sha512-0DS4Nn4rV8qyFlQCpKK8brT61EUtswynrpfFTcgLErcilBIBskSMQ86fO2WVuybr14ywyKdRjv91FiRZwnEuvQ==}
+ fetch-blob@3.2.0:
+ resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
+ engines: {node: ^12.20 || >= 14.13}
+
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@@ -8900,6 +8986,10 @@ packages:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
+ formdata-polyfill@4.0.10:
+ resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
+ engines: {node: '>=12.20.0'}
+
forwarded-parse@2.1.2:
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
@@ -8977,6 +9067,14 @@ packages:
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
deprecated: This package is no longer supported.
+ gaxios@7.1.3:
+ resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
+ engines: {node: '>=18'}
+
+ gcp-metadata@8.1.2:
+ resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
+ engines: {node: '>=18'}
+
generator-function@2.0.1:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
engines: {node: '>= 0.4'}
@@ -9054,6 +9152,11 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
+ glob@10.5.0:
+ resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ hasBin: true
+
glob@11.1.0:
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
engines: {node: 20 || >=22}
@@ -9115,6 +9218,18 @@ packages:
globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
+ google-auth-library@10.6.1:
+ resolution: {integrity: sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==}
+ engines: {node: '>=18'}
+
+ google-gax@5.0.6:
+ resolution: {integrity: sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==}
+ engines: {node: '>=18'}
+
+ google-logging-utils@1.1.3:
+ resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==}
+ engines: {node: '>=14'}
+
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -9362,6 +9477,10 @@ packages:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
+ http-proxy-agent@5.0.0:
+ resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
+ engines: {node: '>= 6'}
+
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
@@ -9784,6 +9903,9 @@ packages:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'}
+ jackspeak@3.4.3:
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+
jackspeak@4.1.1:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
@@ -9892,6 +10014,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
+ json-bigint@1.0.0:
+ resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
+
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -9950,6 +10075,12 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
+ jwa@2.0.1:
+ resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
+
+ jws@4.0.1:
+ resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
+
katex@0.16.27:
resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==}
hasBin: true
@@ -10124,6 +10255,9 @@ packages:
lodash._pickbycallback@3.0.0:
resolution: {integrity: sha512-DVP27YmN0lB+j/Tgd/+gtxfmW/XihgWpQpHptBuwyp2fD9zEBRwwcnw6Qej16LUV8LRFuTqyoc0i6ON97d/C5w==}
+ lodash.camelcase@4.3.0:
+ resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
+
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
@@ -10176,6 +10310,9 @@ packages:
resolution: {integrity: sha512-p1Ow0C2dDJYaQBhRHt+HVMP6ELuBm4jYSYNHPMfz0J5wJ9qA6/7oBOlBZBfT1InqguTYcvJzNea5FItDxTcbyw==}
hasBin: true
+ long@5.3.2:
+ resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
+
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@@ -10800,6 +10937,11 @@ packages:
node-abort-controller@3.1.1:
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
+ node-domexception@1.0.0:
+ resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+ engines: {node: '>=10.5.0'}
+ deprecated: Use your platform's native DOMException instead
+
node-fetch-native@1.6.7:
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
@@ -10812,6 +10954,10 @@ packages:
encoding:
optional: true
+ node-fetch@3.3.2:
+ resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
node-forge@1.3.3:
resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
engines: {node: '>= 6.13.0'}
@@ -10900,6 +11046,10 @@ packages:
object-deep-merge@2.0.0:
resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==}
+ object-hash@3.0.0:
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
+ engines: {node: '>= 6'}
+
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@@ -11429,6 +11579,14 @@ packages:
proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
+ proto3-json-serializer@3.0.4:
+ resolution: {integrity: sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==}
+ engines: {node: '>=18'}
+
+ protobufjs@7.5.4:
+ resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
+ engines: {node: '>=12.0.0'}
+
protolint@0.56.4:
resolution: {integrity: sha512-wrRXaiyNDSzYJ7LBcDnwkWnsRi1uNlFleQp90CsBsh2YvVJEwKXr/c/W9MRYdt+ScpEo8Eg3d60QmVhsZBJu2w==}
hasBin: true
@@ -11970,6 +12128,10 @@ packages:
retext@9.0.0:
resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==}
+ retry-request@8.0.2:
+ resolution: {integrity: sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==}
+ engines: {node: '>=18'}
+
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -11979,6 +12141,10 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
+ rimraf@5.0.10:
+ resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
+ hasBin: true
+
robust-predicates@3.0.2:
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
@@ -12362,9 +12528,15 @@ packages:
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
engines: {node: '>= 0.10.0'}
+ stream-events@1.0.5:
+ resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==}
+
stream-replace-string@2.0.0:
resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==}
+ stream-shift@1.0.3:
+ resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
+
strict-uri-encode@2.0.0:
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
engines: {node: '>=4'}
@@ -12452,6 +12624,9 @@ packages:
structured-headers@0.4.1:
resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==}
+ stubs@3.0.0:
+ resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==}
+
sturdy-websocket@0.2.1:
resolution: {integrity: sha512-NnzSOEKyv4I83qbuKw9ROtJrrT6Z/Xt7I0HiP/e6H6GnpeTDvzwGIGeJ8slai+VwODSHQDooW2CAilJwT9SpRg==}
@@ -12548,6 +12723,10 @@ packages:
resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==}
engines: {node: '>=18'}
+ teeny-request@10.1.0:
+ resolution: {integrity: sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==}
+ engines: {node: '>=18'}
+
temp-dir@1.0.0:
resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==}
engines: {node: '>=4'}
@@ -13280,6 +13459,10 @@ packages:
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
+ web-streams-polyfill@3.3.3:
+ resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
+ engines: {node: '>= 8'}
+
webauthn-owner-plugin@https://codeload.github.com/exactly/webauthn-owner-plugin/tar.gz/9c0c38bd63c2aa70b60c03c815e9de108e264cda:
resolution: {tarball: https://codeload.github.com/exactly/webauthn-owner-plugin/tar.gz/9c0c38bd63c2aa70b60c03c815e9de108e264cda}
version: 0.0.0
@@ -16333,6 +16516,24 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
+ '@google-cloud/kms@5.4.0':
+ dependencies:
+ google-gax: 5.0.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@grpc/grpc-js@1.14.3':
+ dependencies:
+ '@grpc/proto-loader': 0.8.0
+ '@js-sdsl/ordered-map': 4.4.2
+
+ '@grpc/proto-loader@0.8.0':
+ dependencies:
+ lodash.camelcase: 4.3.0
+ long: 5.3.2
+ protobufjs: 7.5.4
+ yargs: 17.7.2
+
'@hapi/address@5.1.1':
dependencies:
'@hapi/hoek': 11.0.7
@@ -16700,6 +16901,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@js-sdsl/ordered-map@4.4.2': {}
+
'@jsdevtools/ono@7.1.3': {}
'@levischuck/tiny-cbor@0.2.11': {}
@@ -17485,6 +17688,9 @@ snapshots:
transitivePeerDependencies:
- buffer
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
'@pkgr/core@0.1.2': {}
'@pkgr/core@0.2.9': {}
@@ -17510,6 +17716,29 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@protobufjs/aspromise@1.1.2': {}
+
+ '@protobufjs/base64@1.1.2': {}
+
+ '@protobufjs/codegen@2.0.4': {}
+
+ '@protobufjs/eventemitter@1.1.0': {}
+
+ '@protobufjs/fetch@1.1.0':
+ dependencies:
+ '@protobufjs/aspromise': 1.1.2
+ '@protobufjs/inquire': 1.1.0
+
+ '@protobufjs/float@1.0.2': {}
+
+ '@protobufjs/inquire@1.1.0': {}
+
+ '@protobufjs/path@1.1.2': {}
+
+ '@protobufjs/pool@1.1.0': {}
+
+ '@protobufjs/utf8@1.1.0': {}
+
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-collection@1.1.7(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
@@ -19913,6 +20142,8 @@ snapshots:
'@tanstack/store@0.8.0': {}
+ '@tootallnate/once@2.0.0': {}
+
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@@ -20440,6 +20671,15 @@ snapshots:
dependencies:
valibot: 1.2.0(typescript@5.9.3)
+ '@valora/viem-account-hsm-gcp@1.2.17(viem@2.46.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.5))':
+ dependencies:
+ '@google-cloud/kms': 5.4.0
+ '@noble/curves': 1.9.7
+ asn1js: 3.0.7
+ viem: 2.46.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.5)
+ transitivePeerDependencies:
+ - supports-color
+
'@vitest/coverage-v8@4.0.17(vitest@4.0.17)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
@@ -21263,6 +21503,8 @@ snapshots:
big-integer@1.6.52: {}
+ bignumber.js@9.3.1: {}
+
birecord@0.1.1: {}
bl@4.1.0:
@@ -21360,6 +21602,8 @@ snapshots:
once: 1.4.0
sliced: 1.0.1
+ buffer-equal-constant-time@1.0.1: {}
+
buffer-from@1.1.2: {}
buffer@5.7.1:
@@ -22131,6 +22375,8 @@ snapshots:
damerau-levenshtein@1.0.8: {}
+ data-uri-to-buffer@4.0.1: {}
+
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -22330,8 +22576,19 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
+ duplexify@4.1.3:
+ dependencies:
+ end-of-stream: 1.4.5
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+ stream-shift: 1.0.3
+
eastasianwidth@0.2.0: {}
+ ecdsa-sig-formatter@1.0.11:
+ dependencies:
+ safe-buffer: 5.2.1
+
edit-json-file@1.8.1:
dependencies:
find-value: 1.0.13
@@ -23712,6 +23969,11 @@ snapshots:
sprintf-js: 1.1.3
tmp: 0.2.5
+ fetch-blob@3.2.0:
+ dependencies:
+ node-domexception: 1.0.0
+ web-streams-polyfill: 3.3.3
+
fflate@0.8.2: {}
figures@3.2.0:
@@ -23860,6 +24122,10 @@ snapshots:
format@0.2.2: {}
+ formdata-polyfill@4.0.10:
+ dependencies:
+ fetch-blob: 3.2.0
+
forwarded-parse@2.1.2: {}
forwarded@0.2.0: {}
@@ -23949,6 +24215,23 @@ snapshots:
strip-ansi: 6.0.1
wide-align: 1.1.5
+ gaxios@7.1.3:
+ dependencies:
+ extend: 3.0.2
+ https-proxy-agent: 7.0.6
+ node-fetch: 3.3.2
+ rimraf: 5.0.10
+ transitivePeerDependencies:
+ - supports-color
+
+ gcp-metadata@8.1.2:
+ dependencies:
+ gaxios: 7.1.3
+ google-logging-utils: 1.1.3
+ json-bigint: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+
generator-function@2.0.1: {}
gensequence@8.0.8: {}
@@ -24016,6 +24299,15 @@ snapshots:
dependencies:
is-glob: 4.0.3
+ glob@10.5.0:
+ dependencies:
+ foreground-child: 3.3.1
+ jackspeak: 3.4.3
+ minimatch: 9.0.9
+ minipass: 7.1.2
+ package-json-from-dist: 1.0.1
+ path-scurry: 1.11.1
+
glob@11.1.0:
dependencies:
foreground-child: 3.3.1
@@ -24095,6 +24387,35 @@ snapshots:
globrex@0.1.2: {}
+ google-auth-library@10.6.1:
+ dependencies:
+ base64-js: 1.5.1
+ ecdsa-sig-formatter: 1.0.11
+ gaxios: 7.1.3
+ gcp-metadata: 8.1.2
+ google-logging-utils: 1.1.3
+ jws: 4.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ google-gax@5.0.6:
+ dependencies:
+ '@grpc/grpc-js': 1.14.3
+ '@grpc/proto-loader': 0.8.0
+ duplexify: 4.1.3
+ google-auth-library: 10.6.1
+ google-logging-utils: 1.1.3
+ node-fetch: 3.3.2
+ object-hash: 3.0.0
+ proto3-json-serializer: 3.0.4
+ protobufjs: 7.5.4
+ retry-request: 8.0.2
+ rimraf: 5.0.10
+ transitivePeerDependencies:
+ - supports-color
+
+ google-logging-utils@1.1.3: {}
+
gopd@1.2.0: {}
got-fetch@5.1.10(got@12.6.1):
@@ -24444,6 +24765,14 @@ snapshots:
statuses: 2.0.2
toidentifier: 1.0.1
+ http-proxy-agent@5.0.0:
+ dependencies:
+ '@tootallnate/once': 2.0.0
+ agent-base: 6.0.2
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
@@ -24863,6 +25192,12 @@ snapshots:
has-symbols: 1.1.0
set-function-name: 2.0.2
+ jackspeak@3.4.3:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
@@ -25002,6 +25337,10 @@ snapshots:
jsesc@3.1.0: {}
+ json-bigint@1.0.0:
+ dependencies:
+ bignumber.js: 9.3.1
+
json-buffer@3.0.1: {}
json-parse-better-errors@1.0.2: {}
@@ -25057,6 +25396,17 @@ snapshots:
object.assign: 4.1.7
object.values: 1.2.1
+ jwa@2.0.1:
+ dependencies:
+ buffer-equal-constant-time: 1.0.1
+ ecdsa-sig-formatter: 1.0.11
+ safe-buffer: 5.2.1
+
+ jws@4.0.1:
+ dependencies:
+ jwa: 2.0.1
+ safe-buffer: 5.2.1
+
katex@0.16.27:
dependencies:
commander: 8.3.0
@@ -25209,6 +25559,8 @@ snapshots:
lodash._basefor: 3.0.3
lodash.keysin: 3.0.8
+ lodash.camelcase@4.3.0: {}
+
lodash.debounce@4.0.8: {}
lodash.defaults@4.2.0: {}
@@ -25258,6 +25610,8 @@ snapshots:
split: 0.2.10
through: 2.3.8
+ long@5.3.2: {}
+
longest-streak@3.1.0: {}
loose-envify@1.4.0:
@@ -26419,12 +26773,20 @@ snapshots:
node-abort-controller@3.1.1: {}
+ node-domexception@1.0.0: {}
+
node-fetch-native@1.6.7: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
+ node-fetch@3.3.2:
+ dependencies:
+ data-uri-to-buffer: 4.0.1
+ fetch-blob: 3.2.0
+ formdata-polyfill: 4.0.10
+
node-forge@1.3.3: {}
node-gyp-build-optional-packages@5.2.2:
@@ -26545,6 +26907,8 @@ snapshots:
object-deep-merge@2.0.0: {}
+ object-hash@3.0.0: {}
+
object-inspect@1.13.4: {}
object-keys@1.1.1: {}
@@ -27115,6 +27479,25 @@ snapshots:
proto-list@1.2.4: {}
+ proto3-json-serializer@3.0.4:
+ dependencies:
+ protobufjs: 7.5.4
+
+ protobufjs@7.5.4:
+ dependencies:
+ '@protobufjs/aspromise': 1.1.2
+ '@protobufjs/base64': 1.1.2
+ '@protobufjs/codegen': 2.0.4
+ '@protobufjs/eventemitter': 1.1.0
+ '@protobufjs/fetch': 1.1.0
+ '@protobufjs/float': 1.0.2
+ '@protobufjs/inquire': 1.1.0
+ '@protobufjs/path': 1.1.2
+ '@protobufjs/pool': 1.1.0
+ '@protobufjs/utf8': 1.1.0
+ '@types/node': 25.0.9
+ long: 5.3.2
+
protolint@0.56.4:
dependencies:
got: 12.6.1
@@ -27805,12 +28188,23 @@ snapshots:
retext-stringify: 4.0.0
unified: 11.0.5
+ retry-request@8.0.2:
+ dependencies:
+ extend: 3.0.2
+ teeny-request: 10.1.0
+ transitivePeerDependencies:
+ - supports-color
+
reusify@1.1.0: {}
rimraf@3.0.2:
dependencies:
glob: 7.2.3
+ rimraf@5.0.10:
+ dependencies:
+ glob: 10.5.0
+
robust-predicates@3.0.2: {}
rollup-pluginutils@2.8.2:
@@ -28328,8 +28722,14 @@ snapshots:
stream-buffers@2.2.0: {}
+ stream-events@1.0.5:
+ dependencies:
+ stubs: 3.0.0
+
stream-replace-string@2.0.0: {}
+ stream-shift@1.0.3: {}
+
strict-uri-encode@2.0.0: {}
string-ts@2.3.1: {}
@@ -28440,6 +28840,8 @@ snapshots:
structured-headers@0.4.1: {}
+ stubs@3.0.0: {}
+
sturdy-websocket@0.2.1:
optional: true
@@ -28612,6 +29014,15 @@ snapshots:
minizlib: 3.1.0
yallist: 5.0.0
+ teeny-request@10.1.0:
+ dependencies:
+ http-proxy-agent: 5.0.0
+ https-proxy-agent: 5.0.1
+ node-fetch: 3.3.2
+ stream-events: 1.0.5
+ transitivePeerDependencies:
+ - supports-color
+
temp-dir@1.0.0: {}
temp-dir@2.0.0: {}
@@ -29265,6 +29676,8 @@ snapshots:
web-namespaces@2.0.1: {}
+ web-streams-polyfill@3.3.3: {}
+
webauthn-owner-plugin@https://codeload.github.com/exactly/webauthn-owner-plugin/tar.gz/9c0c38bd63c2aa70b60c03c815e9de108e264cda: {}
webauthn-p256@0.0.10:
diff --git a/server/hooks/activity.ts b/server/hooks/activity.ts
index 1e1dfa3b1..e27aa8cd2 100644
--- a/server/hooks/activity.ts
+++ b/server/hooks/activity.ts
@@ -15,25 +15,21 @@ import createDebug from "debug";
import { eq, inArray } from "drizzle-orm";
import { Hono } from "hono";
import * as v from "valibot";
-import { bytesToBigInt, hexToBigInt, withRetry } from "viem";
+import { bytesToBigInt, hexToBigInt } from "viem";
import {
- auditorAbi,
exaAccountFactoryAbi,
- exaPluginAbi,
exaPreviewerAbi,
exaPreviewerAddress,
- marketAbi,
- upgradeableModularAccountAbi,
wethAddress,
} from "@exactly/common/generated/chain";
import { Address, Hash, Hex } from "@exactly/common/validation";
import database, { cards, credentials } from "../database";
+import { keeper } from "../utils/accounts";
import { createWebhook, findWebhook, headerValidator, network } from "../utils/alchemy";
import appOrigin from "../utils/appOrigin";
import decodePublicKey from "../utils/decodePublicKey";
-import keeper from "../utils/keeper";
import { sendPushNotification } from "../utils/onesignal";
import { autoCredit } from "../utils/panda";
import publicClient from "../utils/publicClient";
@@ -96,7 +92,7 @@ export default new Hono().post(
category !== "erc1155" &&
(rawContract?.rawValue && rawContract.rawValue !== "0x" ? hexToBigInt(rawContract.rawValue) > 0n : !!value),
);
- const accounts = await database.query.credentials
+ const accountLookup = await database.query.credentials
.findMany({
columns: { account: true, publicKey: true, factory: true, source: true },
where: inArray(credentials.account, [...new Set(transfers.map(({ toAddress }) => toAddress))]),
@@ -109,18 +105,16 @@ export default new Hono().post(
),
),
);
- if (Object.keys(accounts).length === 1) setUser({ id: v.parse(Address, Object.keys(accounts)[0]) });
+ if (Object.keys(accountLookup).length === 1) setUser({ id: v.parse(Address, Object.keys(accountLookup)[0]) });
const marketsByAsset = await publicClient
.readContract({ address: exaPreviewerAddress, functionName: "assets", abi: exaPreviewerAbi })
.then((p) => new Map
(p.map((m) => [v.parse(Address, m.asset), v.parse(Address, m.market)])));
const markets = new Set(marketsByAsset.values());
- const pokes = new Map<
- Address,
- { assets: Set; factory: Address; publicKey: Uint8Array; source: null | string }
- >();
+
+ const accounts = new Set();
for (const { toAddress: account, rawContract, value, asset: assetSymbol } of transfers) {
- if (!accounts[account]) continue;
+ if (!accountLookup[account]) continue;
if (rawContract?.address && markets.has(rawContract.address)) continue;
const asset = rawContract?.address ?? ETH;
const underlying = asset === ETH ? WETH : asset;
@@ -131,141 +125,84 @@ export default new Hono().post(
en: `${value ? `${value} ` : ""}${assetSymbol} received${marketsByAsset.has(underlying) ? " and instantly started earning yield" : ""}`,
},
}).catch((error: unknown) => captureException(error));
-
- if (pokes.has(account)) {
- pokes.get(account)?.assets.add(asset);
- } else {
- const { publicKey, factory, source } = accounts[account];
- pokes.set(account, { publicKey, factory, source, assets: new Set([asset]) });
- }
+ accounts.add(account);
}
const { "sentry-trace": sentryTrace, baggage } = getTraceData();
Promise.allSettled(
- [...pokes].map(([account, { publicKey, factory, source, assets }]) =>
- continueTrace({ sentryTrace, baggage }, () =>
- withScope((scope) =>
- startSpan(
- { name: "account activity", op: "exa.activity", attributes: { account }, forceTransaction: true },
- async (span) => {
- scope.setUser({ id: account });
- const isDeployed = !!(await publicClient.getCode({ address: account }));
- scope.setTag("exa.new", !isDeployed);
- if (!isDeployed) {
- try {
- await keeper.exaSend(
- { name: "create account", op: "exa.account", attributes: { account } },
- {
- address: factory,
- functionName: "createAccount",
- args: [0n, [decodePublicKey(publicKey, bytesToBigInt)]],
- abi: exaAccountFactoryAbi,
- },
- );
- track({ event: "AccountFunded", userId: account, properties: { source } });
- } catch (error: unknown) {
- span.setStatus({ code: SPAN_STATUS_ERROR, message: "account_failed" });
- throw error;
- }
- }
- if (assets.has(ETH)) assets.delete(WETH);
- const results = await Promise.allSettled(
- [...assets]
- .filter((asset) => marketsByAsset.has(asset) || asset === ETH)
- .map(async (asset) =>
- withRetry(
- () =>
- keeper
- .exaSend(
- { name: "poke account", op: "exa.poke", attributes: { account, asset } },
- {
- address: account,
- abi: [...exaPluginAbi, ...upgradeableModularAccountAbi, ...auditorAbi, ...marketAbi],
- ...(asset === ETH
- ? { functionName: "pokeETH" }
- : {
- functionName: "poke",
- args: [marketsByAsset.get(asset)!], // eslint-disable-line @typescript-eslint/no-non-null-assertion
- }),
- },
- { ignore: ["NoBalance()"] },
- )
- .then((receipt) => {
- if (receipt) return receipt;
- throw new Error("NoBalance()");
- }),
+ [...accounts]
+ .flatMap((account) => {
+ const info = accountLookup[account];
+ return info ? [[account, info] as const] : [];
+ })
+ .map(([account, { publicKey, factory, source }]) =>
+ continueTrace({ sentryTrace, baggage }, () =>
+ withScope((scope) =>
+ startSpan(
+ { name: "account activity", op: "exa.activity", attributes: { account }, forceTransaction: true },
+ async (span) => {
+ scope.setUser({ id: account });
+ scope.setTag("exa.account", account);
+ const isDeployed = !!(await publicClient.getCode({ address: account }));
+ scope.setTag("exa.new", !isDeployed);
+ if (!isDeployed) {
+ try {
+ await keeper.exaSend(
+ { name: "create account", op: "exa.account", attributes: { account } },
{
- delay: 2000,
- retryCount: 5,
- shouldRetry: ({ error }) => {
- if (error instanceof Error && error.message === "NoBalance()") return true;
- withScope((captureScope) => {
- captureScope.setUser({ id: account });
- captureException(error, { level: "error", fingerprint: revertFingerprint(error) });
- });
- return true;
- },
+ address: factory,
+ functionName: "createAccount",
+ args: [0n, [decodePublicKey(publicKey, bytesToBigInt)]],
+ abi: exaAccountFactoryAbi,
},
- ),
- ),
- );
- for (const result of results) {
- if (result.status === "fulfilled") continue;
- if (result.reason instanceof Error && result.reason.message === "NoBalance()") {
- withScope((captureScope) => {
- captureScope.setUser({ id: account });
- captureScope.addEventProcessor((event) => {
- if (event.exception?.values?.[0]) event.exception.values[0].type = "NoBalance";
- return event;
- });
- captureException(result.reason, {
- level: "warning",
- fingerprint: ["{{ default }}", "NoBalance"],
- });
- });
- continue;
+ );
+ track({ event: "AccountFunded", userId: account, properties: { source } });
+ } catch (error: unknown) {
+ span.setStatus({ code: SPAN_STATUS_ERROR, message: "account_failed" });
+ throw error;
+ }
}
- span.setStatus({ code: SPAN_STATUS_ERROR, message: "poke_failed" });
- throw result.reason;
- }
- autoCredit(account)
- .then(async (auto) => {
- span.setAttribute("exa.autoCredit", auto);
- if (!auto) return;
- const credential = await database.query.credentials.findFirst({
- where: eq(credentials.account, account),
- columns: {},
- with: {
- cards: {
- columns: { id: true, mode: true },
- where: inArray(cards.status, ["ACTIVE", "FROZEN"]),
+ await keeper
+ .poke(account, { ignore: [`NotAllowed(${account})`] })
+ .catch((error: unknown) => captureException(error, { level: "error" }));
+ autoCredit(account)
+ .then(async (auto) => {
+ span.setAttribute("exa.autoCredit", auto);
+ if (!auto) return;
+ const credential = await database.query.credentials.findFirst({
+ where: eq(credentials.account, account),
+ columns: {},
+ with: {
+ cards: {
+ columns: { id: true, mode: true },
+ where: inArray(cards.status, ["ACTIVE", "FROZEN"]),
+ },
},
- },
- });
- if (!credential || credential.cards.length === 0) return;
- const card = credential.cards[0];
- span.setAttribute("exa.card", card?.id);
- if (card?.mode !== 0) return;
- await database.update(cards).set({ mode: 1 }).where(eq(cards.id, card.id));
- span.setAttribute("exa.mode", 1);
- sendPushNotification({
- userId: account,
- headings: { en: "Card mode changed" },
- contents: { en: "Credit mode activated" },
- }).catch((error: unknown) => captureException(error));
- })
- .catch((error: unknown) => captureException(error));
- span.setStatus({ code: SPAN_STATUS_OK });
- },
+ });
+ if (!credential || credential.cards.length === 0) return;
+ const card = credential.cards[0];
+ span.setAttribute("exa.card", card?.id);
+ if (card?.mode !== 0) return;
+ await database.update(cards).set({ mode: 1 }).where(eq(cards.id, card.id));
+ span.setAttribute("exa.mode", 1);
+ sendPushNotification({
+ userId: account,
+ headings: { en: "Card mode changed" },
+ contents: { en: "Credit mode activated" },
+ }).catch((error: unknown) => captureException(error, { level: "error" }));
+ })
+ .catch((error: unknown) => captureException(error, { level: "error" }));
+ span.setStatus({ code: SPAN_STATUS_OK });
+ },
+ ),
),
- ),
- ).catch((error: unknown) => {
- withScope((scope) => {
- scope.setUser({ id: account });
- captureException(error, { level: "error", fingerprint: revertFingerprint(error) });
- });
- throw error;
- }),
- ),
+ ).catch((error: unknown) => {
+ withScope((captureScope) => {
+ captureScope.setUser({ id: account });
+ captureException(error, { level: "error", fingerprint: revertFingerprint(error) });
+ });
+ throw error;
+ }),
+ ),
)
.then((results) => {
getActiveSpan()?.setStatus(
@@ -274,7 +211,7 @@ export default new Hono().post(
: { code: SPAN_STATUS_ERROR, message: "activity_failed" },
);
})
- .catch((error: unknown) => captureException(error));
+ .catch((error: unknown) => captureException(error, { level: "error" }));
return c.json({});
},
);
diff --git a/server/hooks/block.ts b/server/hooks/block.ts
index 82a4b0bef..d5517d149 100644
--- a/server/hooks/block.ts
+++ b/server/hooks/block.ts
@@ -46,10 +46,10 @@ import revertReason from "@exactly/common/revertReason";
import shortenHex from "@exactly/common/shortenHex";
import { Address, Hash, Hex } from "@exactly/common/validation";
+import { keeper } from "../utils/accounts";
import { headers as alchemyHeaders, createWebhook, findWebhook, headerValidator } from "../utils/alchemy";
import appOrigin from "../utils/appOrigin";
import ensClient from "../utils/ensClient";
-import keeper from "../utils/keeper";
import { sendPushNotification } from "../utils/onesignal";
import publicClient from "../utils/publicClient";
import redis from "../utils/redis";
diff --git a/server/hooks/panda.ts b/server/hooks/panda.ts
index 348e55731..c50f4e3b0 100644
--- a/server/hooks/panda.ts
+++ b/server/hooks/panda.ts
@@ -51,7 +51,7 @@ import { Address, type Hash, type Hex } from "@exactly/common/validation";
import { MATURITY_INTERVAL, splitInstallments } from "@exactly/lib";
import database, { cards, credentials, transactions } from "../database/index";
-import keeper from "../utils/keeper";
+import { keeper } from "../utils/accounts";
import { sendPushNotification } from "../utils/onesignal";
import { collectors, createMutex, getMutex, getUser, headerValidator, signIssuerOp, updateUser } from "../utils/panda";
import publicClient from "../utils/publicClient";
diff --git a/server/hooks/persona.ts b/server/hooks/persona.ts
index a3f0c130a..e97b69be2 100644
--- a/server/hooks/persona.ts
+++ b/server/hooks/persona.ts
@@ -13,6 +13,7 @@ import {
nullable,
object,
optional,
+ parse,
pipe,
safeParse,
string,
@@ -20,9 +21,11 @@ import {
union,
} from "valibot";
+import { firewallAddress } from "@exactly/common/generated/chain";
import { Address } from "@exactly/common/validation";
import database, { credentials } from "../database/index";
+import { allower, keeper } from "../utils/accounts";
import { createUser } from "../utils/panda";
import { addCapita, deriveAssociateId } from "../utils/pax";
import { addDocument, headerValidator, MANTECA_TEMPLATE_WITH_ID_CLASS, PANDA_TEMPLATE } from "../utils/persona";
@@ -30,6 +33,16 @@ import { customer } from "../utils/sardine";
import validatorHook from "../utils/validatorHook";
import type { InferOutput } from "valibot";
+
+let allowerPromise: ReturnType | undefined;
+function getAllower() {
+ allowerPromise ??= allower().catch((error: unknown) => {
+ allowerPromise = undefined;
+ throw error;
+ });
+ return allowerPromise;
+}
+
const Session = pipe(
object({
type: literal("inquiry-session"),
@@ -273,6 +286,24 @@ export default new Hono().post(
if (risk.level === "very_high") return c.json({ code: "very high risk" }, 200);
}
+ const account = parse(Address, credential.account);
+ if (firewallAddress) {
+ try {
+ await getAllower().then((client) => client.allow(account, { ignore: [`AlreadyAllowed(${account})`] }));
+ } catch (error: unknown) {
+ captureException(error, { level: "error" });
+ return c.json({ code: "firewall error" }, 500);
+ }
+ keeper
+ .poke(account, {
+ notification: {
+ headings: { en: "Account assets updated" },
+ contents: { en: "Your funds are ready to use" },
+ },
+ })
+ .catch((error: unknown) => captureException(error, { level: "error" }));
+ }
+
// TODO implement error handling to return 200 if event should not be retried
const { id } = await createUser({
accountPurpose: fields.accountPurpose.value,
@@ -289,26 +320,17 @@ export default new Hono().post(
getActiveSpan()?.setAttributes({ "exa.pandaId": id });
setContext("persona", { inquiryId: personaShareToken, pandaId: id });
- const account = safeParse(Address, credential.account);
- if (account.success) {
- addCapita({
- birthdate: fields.birthdate.value,
- document: fields.identificationNumber.value,
- firstName: fields.nameFirst.value,
- lastName: fields.nameLast.value,
- email: fields.emailAddress.value,
- phone: fields.phoneNumber?.value ?? "",
- internalId: deriveAssociateId(account.output),
- product: "travel insurance",
- }).catch((error: unknown) => {
- captureException(error, { level: "error", extra: { pandaId: id, referenceId } });
- });
- } else {
- captureException(new Error("invalid account address"), {
- extra: { pandaId: id, referenceId, account: credential.account },
- level: "error",
- });
- }
+ addCapita({
+ birthdate: fields.birthdate.value,
+ document: fields.identificationNumber.value,
+ firstName: fields.nameFirst.value,
+ lastName: fields.nameLast.value,
+ email: fields.emailAddress.value,
+ phone: fields.phoneNumber?.value ?? "",
+ internalId: deriveAssociateId(account),
+ product: "travel insurance",
+ }).catch((error: unknown) => captureException(error, { level: "error", extra: { pandaId: id, referenceId } }));
+
addDocument(referenceId, {
id_class: { value: fields.identificationClass.value },
id_number: { value: fields.identificationNumber.value },
diff --git a/server/package.json b/server/package.json
index b5af3a188..c1d9c8a0a 100644
--- a/server/package.json
+++ b/server/package.json
@@ -32,6 +32,7 @@
"dependencies": {
"@account-kit/infra": "catalog:",
"@exactly/lib": "^0.1.0",
+ "@google-cloud/kms": "^5.3.0",
"@hono/node-server": "^1.19.10",
"@hono/sentry": "^1.2.2",
"@hono/valibot-validator": "^0.5.3",
@@ -44,6 +45,7 @@
"@simplewebauthn/server": "^13.2.2",
"@types/debug": "^4.1.12",
"@valibot/to-json-schema": "^1.5.0",
+ "@valora/viem-account-hsm-gcp": "^1.2.16",
"async-mutex": "^0.5.0",
"bullmq": "^5.66.5",
"debug": "^4.4.3",
diff --git a/server/script/openapi.ts b/server/script/openapi.ts
index 124c51240..60c0f59a6 100644
--- a/server/script/openapi.ts
+++ b/server/script/openapi.ts
@@ -29,6 +29,11 @@ process.env.REDIS_URL = "redis";
process.env.SARDINE_API_KEY = "sardine";
process.env.SARDINE_API_URL = "https://api.sardine.ai";
process.env.SEGMENT_WRITE_KEY = "segment";
+process.env.GCP_BASE64_JSON = "base64String==";
+process.env.GOOGLE_APPLICATION_CREDENTIALS = "path/to/credentials.json";
+process.env.GCP_KMS_KEY_RING = "op-sepolia";
+process.env.GCP_KMS_KEY_VERSION = "1";
+process.env.GCP_PROJECT_ID = "exa-dev";
/* eslint-disable n/no-process-exit, unicorn/no-process-exit, no-console -- cli */
import("../api")
diff --git a/server/test/api/card.test.ts b/server/test/api/card.test.ts
index 7f5c24490..e1310ba42 100644
--- a/server/test/api/card.test.ts
+++ b/server/test/api/card.test.ts
@@ -1,7 +1,6 @@
+import "../mocks/accounts";
import "../mocks/auth";
import "../mocks/deployments";
-import "../mocks/keeper";
-import "../mocks/onesignal";
import "../mocks/pax";
import "../mocks/persona";
@@ -20,7 +19,7 @@ import { Address } from "@exactly/common/validation";
import app from "../../api/card";
import database, { cards, credentials } from "../../database";
-import keeper from "../../utils/keeper";
+import { keeper } from "../../utils/accounts";
import * as panda from "../../utils/panda";
import * as pax from "../../utils/pax";
import * as persona from "../../utils/persona";
diff --git a/server/test/e2e.ts b/server/test/e2e.ts
index c5c73ce05..2b426d100 100644
--- a/server/test/e2e.ts
+++ b/server/test/e2e.ts
@@ -1,7 +1,7 @@
///
+import "./mocks/accounts";
import "./mocks/alchemy";
import "./mocks/deployments";
-import "./mocks/keeper";
import "./mocks/onesignal";
import "./mocks/pax";
import "./mocks/redis";
diff --git a/server/test/hooks/activity.test.ts b/server/test/hooks/activity.test.ts
index 5358eae9d..1ec802bbd 100644
--- a/server/test/hooks/activity.test.ts
+++ b/server/test/hooks/activity.test.ts
@@ -1,6 +1,6 @@
+import "../mocks/accounts";
import "../mocks/alchemy";
import "../mocks/deployments";
-import "../mocks/keeper";
import "../mocks/onesignal";
import "../mocks/sentry";
@@ -28,8 +28,8 @@ import { exaAccountFactoryAbi, previewerAbi } from "@exactly/common/generated/ch
import database, { credentials } from "../../database";
import app from "../../hooks/activity";
+import { keeper } from "../../utils/accounts";
import * as decodePublicKey from "../../utils/decodePublicKey";
-import keeper from "../../utils/keeper";
import * as onesignal from "../../utils/onesignal";
import publicClient from "../../utils/publicClient";
import anvilClient from "../anvilClient";
@@ -55,40 +55,6 @@ describe("address activity", () => {
]);
});
- it("captures no balance once after retries", async () => {
- vi.spyOn(publicClient, "getCode").mockResolvedValue("0x1");
- vi.spyOn(keeper, "exaSend").mockImplementation((spanOptions) =>
- Promise.resolve(
- spanOptions.op === "exa.poke" ? null : ({ status: "success" } as Awaited>),
- ),
- );
-
- const response = await appClient.index.$post({
- ...activityPayload,
- json: {
- ...activityPayload.json,
- event: {
- ...activityPayload.json.event,
- activity: [{ ...activityPayload.json.event.activity[0], toAddress: account }],
- },
- },
- });
-
- await vi.waitUntil(
- () => vi.mocked(captureException).mock.calls.some(([error, hint]) => isNoBalance(error, hint, "warning")),
- 26_666,
- );
-
- expect(
- vi.mocked(captureException).mock.calls.filter(([error, hint]) => isNoBalance(error, hint, "warning")),
- ).toHaveLength(1);
- expect(
- vi.mocked(captureException).mock.calls.filter(([error, hint]) => isNoBalance(error, hint, "error")),
- ).toHaveLength(0);
- expect(setUser).toHaveBeenCalledWith({ id: account });
- expect(response.status).toBe(200);
- });
-
it("fails with unexpected error", async () => {
const getCode = vi.spyOn(publicClient, "getCode");
getCode.mockRejectedValue(new Error("Unexpected"));
@@ -310,6 +276,7 @@ describe("address activity", () => {
}),
}),
);
+ await anvilClient.setBalance({ address: account, value: parseEther("5") });
const response = await appClient.index.$post({
...activityPayload,
@@ -342,6 +309,7 @@ describe("address activity", () => {
cause: new ContractFunctionRevertedError({ abi: [], functionName: "pokeETH", message: "custom reason" }),
}),
);
+ await anvilClient.setBalance({ address: account, value: parseEther("5") });
const response = await appClient.index.$post({
...activityPayload,
@@ -374,6 +342,7 @@ describe("address activity", () => {
cause: new ContractFunctionRevertedError({ abi: [], data: "0xdeadbeef", functionName: "pokeETH" }),
}),
);
+ await anvilClient.setBalance({ address: account, value: parseEther("5") });
const response = await appClient.index.$post({
...activityPayload,
@@ -404,6 +373,7 @@ describe("address activity", () => {
vi.spyOn(publicClient, "simulateContract").mockRejectedValueOnce(
new BaseError("test", { cause: new ContractFunctionRevertedError({ abi: [], functionName: "pokeETH" }) }),
);
+ await anvilClient.setBalance({ address: account, value: parseEther("5") });
const response = await appClient.index.$post({
...activityPayload,
@@ -432,6 +402,7 @@ describe("address activity", () => {
it("fingerprints shouldRetry as unknown", async () => {
vi.spyOn(publicClient, "getCode").mockResolvedValue("0x1");
vi.spyOn(publicClient, "simulateContract").mockRejectedValueOnce(new Error("unexpected"));
+ await anvilClient.setBalance({ address: account, value: parseEther("5") });
const response = await appClient.index.$post({
...activityPayload,
@@ -505,7 +476,7 @@ describe("address activity", () => {
});
it("pokes eth with value when rawValue is 0x", async () => {
- const exaSend = vi.spyOn(keeper, "exaSend");
+ const pokeSpy = vi.spyOn(keeper, "poke");
const deposit = parseEther("5");
await anvilClient.setBalance({ address: account, value: deposit });
@@ -525,22 +496,14 @@ describe("address activity", () => {
waitForWETHMarket(account, deposit),
]);
- expect(
- exaSend.mock.calls.some(
- ([spanOptions, request]) =>
- spanOptions.op === "exa.poke" &&
- request.address === account &&
- "functionName" in request &&
- request.functionName === "pokeETH",
- ),
- ).toBe(true);
+ expect(pokeSpy).toHaveBeenCalledWith(account, expect.objectContaining({ ignore: [`NotAllowed(${account})`] }));
expect(market.floatingDepositAssets).toBe(deposit);
expect(market.isCollateral).toBe(true);
expect(response.status).toBe(200);
});
it("pokes eth without value", async () => {
- const exaSend = vi.spyOn(keeper, "exaSend");
+ const pokeSpy = vi.spyOn(keeper, "poke");
const deposit = parseEther("5");
await anvilClient.setBalance({ address: account, value: deposit });
@@ -568,15 +531,7 @@ describe("address activity", () => {
waitForWETHMarket(account, deposit),
]);
- expect(
- exaSend.mock.calls.some(
- ([spanOptions, request]) =>
- spanOptions.op === "exa.poke" &&
- request.address === account &&
- "functionName" in request &&
- request.functionName === "pokeETH",
- ),
- ).toBe(true);
+ expect(pokeSpy).toHaveBeenCalledWith(account, expect.objectContaining({ ignore: [`NotAllowed(${account})`] }));
expect(market.floatingDepositAssets).toBe(deposit);
expect(market.isCollateral).toBe(true);
expect(response.status).toBe(200);
@@ -620,7 +575,7 @@ describe("address activity", () => {
});
it("pokes token without value", async () => {
- const exaSend = vi.spyOn(keeper, "exaSend");
+ const pokeSpy = vi.spyOn(keeper, "poke");
const weth = parseEther("2");
await keeper.exaSend(
{ name: "mint", op: "tx.mint" },
@@ -651,15 +606,7 @@ describe("address activity", () => {
waitForWETHMarket(account, weth),
]);
- expect(
- exaSend.mock.calls.some(
- ([spanOptions, request]) =>
- spanOptions.op === "exa.poke" &&
- request.address === account &&
- "functionName" in request &&
- request.functionName === "poke",
- ),
- ).toBe(true);
+ expect(pokeSpy).toHaveBeenCalledWith(account, expect.objectContaining({ ignore: [`NotAllowed(${account})`] }));
expect(market.floatingDepositAssets).toBe(weth);
expect(market.isCollateral).toBe(true);
expect(response.status).toBe(200);
@@ -807,6 +754,27 @@ describe("address activity", () => {
expect(setUser).toHaveBeenCalledWith({ id: account });
expect(response.status).toBe(200);
});
+
+ it("calls poke with correct ignore option", async () => {
+ const pokeSpy = vi.spyOn(keeper, "poke").mockResolvedValue();
+
+ const response = await appClient.index.$post({
+ ...activityPayload,
+ json: {
+ ...activityPayload.json,
+ event: {
+ ...activityPayload.json.event,
+ activity: [{ ...activityPayload.json.event.activity[0], toAddress: account }],
+ },
+ },
+ });
+
+ expect(response.status).toBe(200);
+
+ await vi.waitUntil(() => pokeSpy.mock.calls.some(([addr]) => addr === account), { timeout: 5000 });
+
+ expect(pokeSpy).toHaveBeenCalledWith(account, { ignore: [`NotAllowed(${account})`] });
+ });
});
async function getWETHMarket(account: Address) {
diff --git a/server/test/hooks/block.test.ts b/server/test/hooks/block.test.ts
index f3322200d..16763297b 100644
--- a/server/test/hooks/block.test.ts
+++ b/server/test/hooks/block.test.ts
@@ -48,8 +48,8 @@ import ProposalType, { decodeWithdraw } from "@exactly/common/ProposalType";
import deploy from "@exactly/plugin/deploy.json";
import app from "../../hooks/block";
+import { keeper } from "../../utils/accounts";
import ensClient from "../../utils/ensClient";
-import keeper from "../../utils/keeper";
import * as onesignal from "../../utils/onesignal";
import publicClient from "../../utils/publicClient";
import redis from "../../utils/redis";
diff --git a/server/test/hooks/panda.test.ts b/server/test/hooks/panda.test.ts
index 9419097c2..ff5ab955e 100644
--- a/server/test/hooks/panda.test.ts
+++ b/server/test/hooks/panda.test.ts
@@ -1,5 +1,5 @@
+import "../mocks/accounts";
import "../mocks/deployments";
-import "../mocks/keeper";
import "../mocks/onesignal";
import "../mocks/panda";
import "../mocks/redis";
@@ -47,7 +47,7 @@ import { proposalManager } from "@exactly/plugin/deploy.json";
import database, { cards, credentials, transactions } from "../../database";
import app from "../../hooks/panda";
-import keeper from "../../utils/keeper";
+import { keeper } from "../../utils/accounts";
import * as panda from "../../utils/panda";
import publicClient from "../../utils/publicClient";
import * as sardine from "../../utils/sardine";
diff --git a/server/test/hooks/persona.test.ts b/server/test/hooks/persona.test.ts
index 6c3774e02..b45e616a6 100644
--- a/server/test/hooks/persona.test.ts
+++ b/server/test/hooks/persona.test.ts
@@ -5,22 +5,43 @@ import "../mocks/sentry";
import { captureException } from "@sentry/node";
import { eq } from "drizzle-orm";
import { testClient } from "hono/testing";
-import { hexToBytes, padHex, zeroHash } from "viem";
+import { hexToBytes, padHex, parseEther, zeroHash } from "viem";
import { privateKeyToAddress } from "viem/accounts";
import { afterEach, beforeAll, describe, expect, inject, it, vi } from "vitest";
import deriveAddress from "@exactly/common/deriveAddress";
+import { wethAddress } from "@exactly/common/generated/chain";
import database, { credentials } from "../../database";
import app from "../../hooks/persona";
+import { allower, keeper } from "../../utils/accounts";
import * as panda from "../../utils/panda";
import * as pax from "../../utils/pax";
import * as persona from "../../utils/persona";
+import publicClient from "../../utils/publicClient";
import * as sardine from "../../utils/sardine";
+import type * as AccountsModule from "../../utils/accounts";
+
const appClient = testClient(app);
vi.mock("@sentry/node", { spy: true });
+const mockAllow = vi.fn().mockResolvedValue({});
+
+vi.mock("../../utils/accounts", async (importOriginal) => {
+ const original = await importOriginal();
+ return {
+ ...original,
+ allower: vi.fn(() => Promise.resolve({ allow: mockAllow })),
+ };
+});
+vi.mock("@exactly/common/generated/chain", async () => {
+ const actual = await vi.importActual("@exactly/common/generated/chain");
+ return {
+ ...actual,
+ firewallAddress: "0x1234567890123456789012345678901234567890",
+ };
+});
describe("with reference", () => {
const referenceId = "hook-persona";
@@ -38,8 +59,41 @@ describe("with reference", () => {
await database.update(credentials).set({ pandaId: null }).where(eq(credentials.id, referenceId));
});
+ it("returns firewall error when allower initialization fails", async () => {
+ vi.mocked(allower).mockRejectedValueOnce(new Error("allower init failed"));
+ vi.spyOn(sardine, "customer").mockResolvedValueOnce({ sessionKey: "test", status: "Success", level: "low" });
+ const response = await appClient.index.$post({
+ ...personaPayload,
+ json: {
+ ...personaPayload.json,
+ data: {
+ ...personaPayload.json.data,
+ attributes: {
+ ...personaPayload.json.data.attributes,
+ payload: {
+ ...personaPayload.json.data.attributes.payload,
+ data: {
+ ...personaPayload.json.data.attributes.payload.data,
+ attributes: {
+ ...personaPayload.json.data.attributes.payload.data.attributes,
+ referenceId,
+ },
+ },
+ included: [...personaPayload.json.data.attributes.payload.included],
+ },
+ },
+ },
+ },
+ });
+
+ expect(response.status).toBe(500);
+ expect(await response.json()).toEqual({ code: "firewall error" });
+ expect(captureException).toHaveBeenCalledWith(new Error("allower init failed"), { level: "error" });
+ });
+
it("creates a panda account", async () => {
vi.spyOn(panda, "createUser").mockResolvedValueOnce({ id: "pandaId" });
+ vi.spyOn(pax, "addCapita").mockResolvedValue({});
vi.spyOn(sardine, "customer").mockResolvedValueOnce({ sessionKey: "test", status: "Success", level: "low" });
vi.spyOn(persona, "addDocument").mockResolvedValueOnce({ data: { id: "doc_123" } });
const response = await appClient.index.$post({
@@ -381,7 +435,10 @@ describe("persona hook", () => {
});
});
- afterEach(() => vi.resetAllMocks());
+ afterEach(async () => {
+ await database.update(credentials).set({ pandaId: null }).where(eq(credentials.id, "persona-ref"));
+ vi.restoreAllMocks();
+ });
it("creates panda and pax user on valid inquiry", async () => {
vi.spyOn(panda, "createUser").mockResolvedValue({ id: "new-panda-id" });
@@ -389,7 +446,9 @@ describe("persona hook", () => {
vi.spyOn(sardine, "customer").mockResolvedValueOnce({ sessionKey: "test", status: "Success", level: "low" });
const response = await appClient.index.$post({
- header: { "persona-signature": "t=1,v1=sha256" },
+ header: {
+ "persona-signature": "t=1733865120,v1=debbacfe1b0c5f8797a1d68e8428fba435aa4ca3b5d9a328c3c96ee4d04d84df",
+ },
json: {
...validPayload,
data: {
@@ -428,6 +487,271 @@ describe("persona hook", () => {
product: "travel insurance",
});
});
+
+ it("pokes assets when balances are positive", async () => {
+ const account = deriveAddress(inject("ExaAccountFactory"), {
+ x: padHex(privateKeyToAddress(padHex("0x420"))),
+ y: zeroHash,
+ });
+ const pokeSpy = vi.spyOn(keeper, "poke").mockResolvedValue();
+
+ const readContractSpy = vi.spyOn(publicClient, "readContract");
+ readContractSpy
+ .mockResolvedValueOnce([
+ { asset: "0x1234567890123456789012345678901234567890", market: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" },
+ ])
+ .mockResolvedValueOnce(parseEther("2"));
+
+ vi.spyOn(publicClient, "getBalance").mockResolvedValue(parseEther("1"));
+
+ vi.spyOn(panda, "createUser").mockResolvedValue({ id: "new-panda-id" });
+ vi.spyOn(pax, "addCapita").mockResolvedValue({});
+ vi.spyOn(sardine, "customer").mockResolvedValueOnce({ sessionKey: "test", status: "Success", level: "low" });
+
+ const response = await appClient.index.$post({
+ header: {
+ "persona-signature": "t=1733865120,v1=debbacfe1b0c5f8797a1d68e8428fba435aa4ca3b5d9a328c3c96ee4d04d84df",
+ },
+ json: {
+ ...validPayload,
+ data: {
+ ...validPayload.data,
+ attributes: {
+ ...validPayload.data.attributes,
+ payload: {
+ ...validPayload.data.attributes.payload,
+ included: [...validPayload.data.attributes.payload.included],
+ },
+ },
+ },
+ },
+ });
+
+ expect(response.status).toBe(200);
+
+ await vi.waitUntil(() => pokeSpy.mock.calls.length > 0, { timeout: 5000 });
+
+ expect(pokeSpy).toHaveBeenCalledWith(account, {
+ notification: {
+ headings: { en: "Account assets updated" },
+ contents: { en: "Your funds are ready to use" },
+ },
+ });
+ });
+
+ it("pokes only eth when balance is positive", async () => {
+ const account = deriveAddress(inject("ExaAccountFactory"), {
+ x: padHex(privateKeyToAddress(padHex("0x420"))),
+ y: zeroHash,
+ });
+ const pokeSpy = vi.spyOn(keeper, "poke").mockResolvedValue();
+
+ const readContractSpy = vi.spyOn(publicClient, "readContract");
+ readContractSpy
+ .mockResolvedValueOnce([{ asset: wethAddress, market: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" }])
+ .mockResolvedValueOnce(0n);
+
+ vi.spyOn(publicClient, "getBalance").mockResolvedValue(parseEther("1"));
+
+ vi.spyOn(panda, "createUser").mockResolvedValue({ id: "new-panda-id" });
+ vi.spyOn(pax, "addCapita").mockResolvedValue({});
+ vi.spyOn(sardine, "customer").mockResolvedValueOnce({ sessionKey: "test", status: "Success", level: "low" });
+
+ const response = await appClient.index.$post({
+ header: {
+ "persona-signature": "t=1733865120,v1=debbacfe1b0c5f8797a1d68e8428fba435aa4ca3b5d9a328c3c96ee4d04d84df",
+ },
+ json: {
+ ...validPayload,
+ data: {
+ ...validPayload.data,
+ attributes: {
+ ...validPayload.data.attributes,
+ payload: {
+ ...validPayload.data.attributes.payload,
+ included: [...validPayload.data.attributes.payload.included],
+ },
+ },
+ },
+ },
+ });
+
+ expect(response.status).toBe(200);
+
+ await vi.waitUntil(() => pokeSpy.mock.calls.length > 0, { timeout: 5000 });
+
+ expect(pokeSpy).toHaveBeenCalledTimes(1);
+ expect(pokeSpy).toHaveBeenCalledWith(account, {
+ notification: {
+ headings: { en: "Account assets updated" },
+ contents: { en: "Your funds are ready to use" },
+ },
+ });
+ });
+
+ it("skips weth when eth balance is positive", async () => {
+ const account = deriveAddress(inject("ExaAccountFactory"), {
+ x: padHex(privateKeyToAddress(padHex("0x420"))),
+ y: zeroHash,
+ });
+ const pokeSpy = vi.spyOn(keeper, "poke").mockResolvedValue();
+
+ const readContractSpy = vi.spyOn(publicClient, "readContract");
+ readContractSpy
+ .mockResolvedValueOnce([
+ { asset: wethAddress, market: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" },
+ { asset: "0x1234567890123456789012345678901234567890", market: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" },
+ ])
+ .mockResolvedValueOnce(parseEther("5"))
+ .mockResolvedValueOnce(parseEther("2"));
+
+ vi.spyOn(publicClient, "getBalance").mockResolvedValue(parseEther("1"));
+
+ vi.spyOn(panda, "createUser").mockResolvedValue({ id: "new-panda-id" });
+ vi.spyOn(pax, "addCapita").mockResolvedValue({});
+ vi.spyOn(sardine, "customer").mockResolvedValueOnce({ sessionKey: "test", status: "Success", level: "low" });
+
+ const response = await appClient.index.$post({
+ header: {
+ "persona-signature": "t=1733865120,v1=debbacfe1b0c5f8797a1d68e8428fba435aa4ca3b5d9a328c3c96ee4d04d84df",
+ },
+ json: {
+ ...validPayload,
+ data: {
+ ...validPayload.data,
+ attributes: {
+ ...validPayload.data.attributes,
+ payload: {
+ ...validPayload.data.attributes.payload,
+ included: [...validPayload.data.attributes.payload.included],
+ },
+ },
+ },
+ },
+ });
+
+ expect(response.status).toBe(200);
+
+ await vi.waitUntil(() => pokeSpy.mock.calls.length > 0, { timeout: 5000 });
+
+ expect(pokeSpy).toHaveBeenCalledTimes(1);
+ expect(pokeSpy).toHaveBeenCalledWith(account, {
+ notification: {
+ headings: { en: "Account assets updated" },
+ contents: { en: "Your funds are ready to use" },
+ },
+ });
+ });
+
+ it("does not poke when balances are zero", async () => {
+ const exaSendSpy = vi.spyOn(keeper, "exaSend").mockResolvedValue(undefined as never);
+
+ const readContractSpy = vi.spyOn(publicClient, "readContract");
+ readContractSpy
+ .mockResolvedValueOnce([
+ { asset: "0x1234567890123456789012345678901234567890", market: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" },
+ ])
+ .mockResolvedValueOnce(0n);
+
+ vi.spyOn(publicClient, "getBalance").mockResolvedValue(0n);
+
+ vi.spyOn(panda, "createUser").mockResolvedValue({ id: "new-panda-id" });
+ vi.spyOn(pax, "addCapita").mockResolvedValue({});
+ vi.spyOn(sardine, "customer").mockResolvedValueOnce({ sessionKey: "test", status: "Success", level: "low" });
+
+ const response = await appClient.index.$post({
+ header: {
+ "persona-signature": "t=1733865120,v1=debbacfe1b0c5f8797a1d68e8428fba435aa4ca3b5d9a328c3c96ee4d04d84df",
+ },
+ json: {
+ ...validPayload,
+ data: {
+ ...validPayload.data,
+ attributes: {
+ ...validPayload.data.attributes,
+ payload: {
+ ...validPayload.data.attributes.payload,
+ included: [...validPayload.data.attributes.payload.included],
+ },
+ },
+ },
+ },
+ });
+
+ expect(response.status).toBe(200);
+
+ await vi.waitFor(
+ () => {
+ expect(exaSendSpy).not.toHaveBeenCalledWith(expect.objectContaining({ op: "exa.poke" }), expect.anything());
+ },
+ { timeout: 100, interval: 20 },
+ );
+ });
+
+ it("captures exception when keeper.poke fails", async () => {
+ const pokeSpy = vi.spyOn(keeper, "poke").mockRejectedValueOnce(new Error("poke failed"));
+ vi.spyOn(panda, "createUser").mockResolvedValue({ id: "new-panda-id" });
+ vi.spyOn(pax, "addCapita").mockResolvedValue({});
+ vi.spyOn(sardine, "customer").mockResolvedValueOnce({ sessionKey: "test", status: "Success", level: "low" });
+
+ const response = await appClient.index.$post({
+ header: {
+ "persona-signature": "t=1733865120,v1=debbacfe1b0c5f8797a1d68e8428fba435aa4ca3b5d9a328c3c96ee4d04d84df",
+ },
+ json: {
+ ...validPayload,
+ data: {
+ ...validPayload.data,
+ attributes: {
+ ...validPayload.data.attributes,
+ payload: {
+ ...validPayload.data.attributes.payload,
+ included: [...validPayload.data.attributes.payload.included],
+ },
+ },
+ },
+ },
+ });
+
+ expect(response.status).toBe(200);
+ await vi.waitFor(() => {
+ expect(captureException).toHaveBeenCalledWith(
+ new Error("poke failed"),
+ expect.objectContaining({ level: "error" }),
+ );
+ });
+ pokeSpy.mockRestore();
+ });
+
+ it("returns error when firewall call fails", async () => {
+ vi.spyOn(panda, "createUser").mockResolvedValue({ id: "new-panda-id" });
+ vi.spyOn(pax, "addCapita").mockResolvedValue({});
+ vi.spyOn(sardine, "customer").mockResolvedValueOnce({ sessionKey: "test", status: "Success", level: "low" });
+
+ mockAllow.mockRejectedValueOnce(new Error("Firewall error"));
+
+ const response = await appClient.index.$post({
+ header: {
+ "persona-signature": "t=1733865120,v1=debbacfe1b0c5f8797a1d68e8428fba435aa4ca3b5d9a328c3c96ee4d04d84df",
+ },
+ json: {
+ ...validPayload,
+ data: {
+ ...validPayload.data,
+ attributes: {
+ ...validPayload.data.attributes,
+ payload: {
+ ...validPayload.data.attributes.payload,
+ included: [...validPayload.data.attributes.payload.included],
+ },
+ },
+ },
+ },
+ });
+
+ expect(response.status).toBe(500);
+ expect(await response.json()).toEqual({ code: "firewall error" });
+ });
});
describe("manteca template", () => {
@@ -447,6 +771,7 @@ describe("manteca template", () => {
it("handles manteca template and adds document", async () => {
vi.spyOn(persona, "addDocument").mockResolvedValueOnce({ data: { id: "doc_manteca" } });
+ vi.spyOn(panda, "createUser").mockResolvedValue({ id: "should-not-be-called" });
const response = await appClient.index.$post({
header: { "persona-signature": "t=1,v1=sha256" },
diff --git a/server/test/mocks/keeper.ts b/server/test/mocks/accounts.ts
similarity index 79%
rename from server/test/mocks/keeper.ts
rename to server/test/mocks/accounts.ts
index 428dd177e..639f76e6a 100644
--- a/server/test/mocks/keeper.ts
+++ b/server/test/mocks/accounts.ts
@@ -1,3 +1,5 @@
+import "./deployments";
+
import path from "node:path";
import { createWalletClient, http, keccak256, toBytes, type NonceManagerSource } from "viem";
import { privateKeyToAccount } from "viem/accounts";
@@ -7,23 +9,24 @@ import { expect, vi } from "vitest";
import alchemyAPIKey from "@exactly/common/alchemyAPIKey";
import chain from "@exactly/common/generated/chain";
-import type * as keeper from "../../utils/keeper";
+import type * as accounts from "../../utils/accounts";
import type * as nonceManager from "../../utils/nonceManager";
export let keeperClient: ReturnType<
typeof createWalletClient, typeof chain, ReturnType>
>;
-vi.mock("../../utils/keeper", async (importOriginal) => {
- const original = await importOriginal();
+vi.mock("../../utils/accounts", async (importOriginal) => {
+ const original = await importOriginal();
return {
...original,
- default: createWalletClient({
+ allower: vi.fn(() => Promise.resolve({ allow: vi.fn().mockResolvedValue({}) })),
+ keeper: createWalletClient({
chain,
transport: http(`${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`),
account: privateKeyToAccount(
keccak256(toBytes(path.relative(path.resolve(__dirname, ".."), expect.getState().testPath ?? ""))), // eslint-disable-line unicorn/prefer-module
- { nonceManager: original.default.account.nonceManager },
+ { nonceManager: original.keeper.account.nonceManager },
),
}).extend((closureClient) => {
keeperClient = closureClient;
diff --git a/server/test/utils/keeper.test.ts b/server/test/utils/accounts.test.ts
similarity index 98%
rename from server/test/utils/keeper.test.ts
rename to server/test/utils/accounts.test.ts
index a10c147bd..8225394ff 100644
--- a/server/test/utils/keeper.test.ts
+++ b/server/test/utils/accounts.test.ts
@@ -1,5 +1,5 @@
+import { keeperClient, nonceSource } from "../mocks/accounts";
import "../mocks/deployments";
-import { keeperClient, nonceSource } from "../mocks/keeper";
import "../mocks/sentry";
import { captureException, withScope } from "@sentry/node";
@@ -9,13 +9,13 @@ import { afterEach, describe, expect, inject, it, vi } from "vitest";
import { auditorAbi } from "@exactly/common/generated/chain";
-import keeper from "../../utils/keeper";
+import { keeper } from "../../utils/accounts";
import nonceManager from "../../utils/nonceManager";
import publicClient from "../../utils/publicClient";
+import type { Hex } from "@exactly/common/validation";
import type * as sentry from "@sentry/node";
import type * as timers from "node:timers/promises";
-import type { Hex } from "viem";
describe("fault tolerance", () => {
it("recovers if transaction is missing", async () => {
diff --git a/server/test/utils/gcp.test.ts b/server/test/utils/gcp.test.ts
new file mode 100644
index 000000000..15c1a2035
--- /dev/null
+++ b/server/test/utils/gcp.test.ts
@@ -0,0 +1,88 @@
+import { access, writeFile } from "node:fs/promises";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { initializeGcpCredentials, isRetryableKmsError, resetGcpInitialization } from "../../utils/gcp";
+
+vi.mock("node:fs/promises", () => ({
+ writeFile: vi.fn(),
+ access: vi.fn(),
+}));
+
+const mockWriteFile = vi.mocked(writeFile);
+const mockAccess = vi.mocked(access);
+
+describe("gcp credentials security", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ resetGcpInitialization();
+ mockAccess.mockRejectedValue(new Error("File not found"));
+ });
+
+ it("creates credentials file with secure permissions (0o600)", async () => {
+ await initializeGcpCredentials();
+
+ expect(mockWriteFile).toHaveBeenCalledWith("/tmp/gcp-service-account.json", expect.any(String), {
+ mode: 0o600,
+ });
+ });
+
+ it("returns early when credentials already exist", async () => {
+ mockAccess.mockResolvedValue();
+
+ await initializeGcpCredentials();
+
+ expect(mockWriteFile).not.toHaveBeenCalled();
+ });
+});
+
+describe("isRetryableKmsError", () => {
+ it("returns false for non-Error values", () => {
+ expect(isRetryableKmsError("string")).toBe(false);
+ expect(isRetryableKmsError(null)).toBe(false);
+ expect(isRetryableKmsError(42)).toBe(false);
+ });
+
+ it("returns true for numeric gRPC codes (14, 4, 13, 8)", () => {
+ for (const code of [14, 4, 13, 8]) {
+ const error = Object.assign(new Error("grpc error"), { code });
+ expect(isRetryableKmsError(error)).toBe(true);
+ }
+ });
+
+ it("returns false for non-retryable numeric codes", () => {
+ const error = Object.assign(new Error("grpc error"), { code: 3 });
+ expect(isRetryableKmsError(error)).toBe(false);
+ });
+
+ it("returns true for string gRPC codes", () => {
+ for (const code of ["UNAVAILABLE", "DEADLINE_EXCEEDED", "INTERNAL", "RESOURCE_EXHAUSTED"]) {
+ const error = Object.assign(new Error("grpc error"), { code });
+ expect(isRetryableKmsError(error)).toBe(true);
+ }
+ });
+
+ it("returns false for non-retryable string codes", () => {
+ const error = Object.assign(new Error("grpc error"), { code: "PERMISSION_DENIED" });
+ expect(isRetryableKmsError(error)).toBe(false);
+ });
+
+ it("returns true for retryable message substrings", () => {
+ for (const message of ["network error", "request timeout", "service unavailable", "internal error occurred"]) {
+ expect(isRetryableKmsError(new Error(message))).toBe(true);
+ }
+ });
+
+ it("returns true for retryable error names", () => {
+ const networkError = new Error("fail");
+ networkError.name = "NetworkError";
+ expect(isRetryableKmsError(networkError)).toBe(true);
+
+ const timeoutError = new Error("fail");
+ timeoutError.name = "TimeoutError";
+ expect(isRetryableKmsError(timeoutError)).toBe(true);
+ });
+
+ it("returns false for generic errors without retryable signals", () => {
+ expect(isRetryableKmsError(new Error("something went wrong"))).toBe(false);
+ });
+});
diff --git a/server/utils/accounts.ts b/server/utils/accounts.ts
new file mode 100644
index 000000000..43bc5d54e
--- /dev/null
+++ b/server/utils/accounts.ts
@@ -0,0 +1,416 @@
+import { KeyManagementServiceClient } from "@google-cloud/kms";
+import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from "@sentry/core";
+import { captureException, captureMessage, startSpan, withScope } from "@sentry/node";
+import { gcpHsmToAccount } from "@valora/viem-account-hsm-gcp";
+import { setTimeout } from "node:timers/promises";
+import { parse, safeParse } from "valibot";
+import {
+ createWalletClient,
+ encodeFunctionData,
+ erc20Abi,
+ getContractError,
+ http,
+ InvalidInputRpcError,
+ keccak256,
+ RawContractError,
+ WaitForTransactionReceiptTimeoutError,
+ withRetry,
+ type HttpTransport,
+ type LocalAccount,
+ type MaybePromise,
+ type Prettify,
+ type PrivateKeyAccount,
+ type TransactionReceipt,
+ type WalletClient,
+ type WriteContractParameters,
+} from "viem";
+import { privateKeyToAccount } from "viem/accounts";
+
+import alchemyAPIKey from "@exactly/common/alchemyAPIKey";
+import chain, {
+ auditorAbi,
+ exaPluginAbi,
+ exaPreviewerAbi,
+ exaPreviewerAddress,
+ firewallAbi,
+ firewallAddress,
+ marketAbi,
+ upgradeableModularAccountAbi,
+ wethAddress,
+} from "@exactly/common/generated/chain";
+import revertReason from "@exactly/common/revertReason";
+import { Address, Hash } from "@exactly/common/validation";
+
+import { GOOGLE_APPLICATION_CREDENTIALS, hasCredentials, initializeGcpCredentials, isRetryableKmsError } from "./gcp";
+import nonceManager from "./nonceManager";
+import { sendPushNotification } from "./onesignal";
+import publicClient, { captureRequests, Requests } from "./publicClient";
+import revertFingerprint from "./revertFingerprint";
+import traceClient from "./traceClient";
+
+if (!chain.rpcUrls.alchemy.http[0]) throw new Error("missing alchemy rpc url");
+
+if (!process.env.GCP_PROJECT_ID) throw new Error("GCP_PROJECT_ID is required when using GCP KMS");
+const projectId = process.env.GCP_PROJECT_ID;
+if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(projectId)) {
+ throw new Error("GCP_PROJECT_ID must be a valid GCP project ID format");
+}
+
+if (!process.env.GCP_KMS_KEY_RING) throw new Error("GCP_KMS_KEY_RING is required when using GCP KMS");
+const keyRing = process.env.GCP_KMS_KEY_RING;
+if (!process.env.GCP_KMS_KEY_VERSION) throw new Error("GCP_KMS_KEY_VERSION is required when using GCP KMS");
+const version = process.env.GCP_KMS_KEY_VERSION;
+if (!/^\d+$/.test(version)) throw new Error("GCP_KMS_KEY_VERSION must be a numeric version number");
+
+export const keeper = createWalletClient({
+ chain,
+ transport: http(`${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`, {
+ batch: true,
+ async onFetchRequest(request) {
+ const clone = request.clone();
+ captureRequests(parse(Requests, await clone.json()));
+ },
+ }),
+ account: privateKeyToAccount(
+ parse(Hash, process.env.KEEPER_PRIVATE_KEY, {
+ message: "invalid keeper private key",
+ }),
+ { nonceManager },
+ ),
+}).extend(extender);
+
+const ETH = parse(Address, "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee");
+const WETH = parse(Address, wethAddress);
+
+export function extender(client: WalletClient) {
+ const send = withExaSend(client);
+
+ return {
+ ...send,
+ poke: async (
+ accountAddress: Address,
+ options?: { ignore?: string[]; notification?: { contents: { en: string }; headings: { en: string } } },
+ ) => {
+ const combinedAccountAbi = [...exaPluginAbi, ...upgradeableModularAccountAbi, ...auditorAbi, ...marketAbi];
+ const marketsByAsset = await publicClient
+ .readContract({ address: exaPreviewerAddress, functionName: "assets", abi: exaPreviewerAbi })
+ .then((p) => new Map(p.map((m) => [parse(Address, m.asset), parse(Address, m.market)])));
+
+ const assetsToPoke: { asset: Address; market: Address | null }[] = [];
+
+ const balances = await Promise.allSettled([
+ publicClient
+ .getBalance({ address: accountAddress })
+ .then((balance): { asset: Address; balance: bigint; market: Address | null } => ({
+ asset: ETH,
+ market: null,
+ balance,
+ })),
+ ...[...marketsByAsset.entries()].map(async ([asset, market]) => ({
+ asset,
+ market,
+ balance: await publicClient.readContract({
+ address: asset,
+ functionName: "balanceOf",
+ args: [accountAddress],
+ abi: erc20Abi,
+ }),
+ })),
+ ]).then((s) => {
+ return s.flatMap((result) => {
+ if (result.status === "rejected") {
+ captureException(result.reason, { level: "error" });
+ return [];
+ }
+ return [result.value];
+ });
+ });
+
+ const hasETH = balances.some((r) => r.asset === ETH && r.balance > 0n);
+ for (const { asset, market, balance } of balances) {
+ if (hasETH && asset === WETH) continue;
+ if (balance > 0n) assetsToPoke.push({ asset, market });
+ }
+
+ const pokes = await Promise.allSettled(
+ assetsToPoke.map(({ asset, market }) =>
+ withRetry(
+ () =>
+ send.exaSend(
+ {
+ name: "poke account",
+ op: "exa.poke",
+ attributes: { account: accountAddress, asset },
+ },
+ asset === ETH
+ ? {
+ address: accountAddress,
+ abi: combinedAccountAbi,
+ functionName: "pokeETH",
+ }
+ : {
+ address: accountAddress,
+ abi: combinedAccountAbi,
+ functionName: "poke",
+ args: [market],
+ },
+ ...(options?.ignore ? [{ ignore: options.ignore }] : []),
+ ),
+ {
+ retryCount: 10,
+ delay: ({ count }) => Math.trunc(1 << count) * 60,
+ },
+ ),
+ ),
+ ).then((r) => {
+ return r.flatMap((result) => {
+ if (result.status === "rejected") {
+ captureException(result.reason, { level: "error" });
+ return [];
+ }
+
+ return result.value ?? [];
+ });
+ });
+
+ if (options?.notification && pokes.length > 0) {
+ sendPushNotification({
+ userId: accountAddress,
+ headings: options.notification.headings,
+ contents: options.notification.contents,
+ }).catch((error: unknown) => captureException(error, { level: "error" }));
+ }
+ },
+ };
+}
+
+export function withExaSend(
+ client: WalletClient & {
+ account: LocalAccount;
+ },
+) {
+ return {
+ exaSend: async (
+ spanOptions: Prettify[0], "name" | "op"> & { name: string; op: string }>,
+ call: Prettify>,
+ options?: {
+ ignore?: ((reason: string) => MaybePromise) | string[];
+ level?: "error" | "warning" | ((reason: string, error: unknown) => "error" | "warning" | false) | false;
+ onHash?: (hash: Hash) => MaybePromise;
+ onReceipt?: (receipt: TransactionReceipt) => MaybePromise;
+ },
+ ) =>
+ withScope((scope) =>
+ startSpan({ forceTransaction: true, ...spanOptions }, async (span) => {
+ const account = safeParse(Address, spanOptions.attributes?.account);
+ if (account.success) scope.setUser({ id: account.output });
+ try {
+ scope.setContext("tx", { call });
+ span.setAttributes({
+ "tx.call": `${call.functionName}(${call.args?.map(String).join(", ") ?? ""})`,
+ "tx.from": client.account.address,
+ "tx.to": call.address,
+ });
+ const txOptions = {
+ type: "eip1559",
+ maxFeePerGas: 1_000_000_000n,
+ maxPriorityFeePerGas: 1_000_000n,
+ gas: 5_000_000n,
+ } as const;
+ const { request: writeRequest } = await startSpan({ name: "eth_call", op: "tx.simulate" }, () =>
+ publicClient.simulateContract({ account: client.account, ...txOptions, ...call }),
+ );
+ const {
+ abi: _,
+ account: __,
+ address: ___,
+ ...request
+ } = { from: writeRequest.account.address, to: writeRequest.address, ...writeRequest };
+ scope.setContext("tx", { request });
+ const prepared = await startSpan({ name: "prepare transaction", op: "tx.prepare" }, () =>
+ client.prepareTransactionRequest({
+ to: call.address,
+ data: encodeFunctionData(call),
+ ...txOptions,
+ nonceManager,
+ }),
+ );
+ scope.setContext("tx", { request, prepared });
+ span.setAttribute("tx.nonce", prepared.nonce);
+ const serializedTransaction = await startSpan({ name: "sign transaction", op: "tx.sign" }, () =>
+ client.signTransaction(prepared),
+ );
+ const hash = keccak256(serializedTransaction);
+ scope.setContext("tx", { request, prepared, hash });
+ span.setAttribute("tx.hash", hash);
+ const abortController = new AbortController();
+ const [, receiptResult] = await Promise.allSettled([
+ (async () => {
+ while (!abortController.signal.aborted) {
+ await Promise.allSettled([
+ startSpan({ name: "send transaction", op: "tx.send" }, () =>
+ publicClient.sendRawTransaction({ serializedTransaction }),
+ ).catch((error: unknown) => {
+ captureException(error, { level: "error" });
+ throw error;
+ }),
+ setTimeout(10_000, null, { signal: abortController.signal }),
+ ]);
+ }
+ })(),
+ startSpan({ name: "wait for receipt", op: "tx.wait" }, () =>
+ publicClient.waitForTransactionReceipt({ hash, confirmations: 0 }),
+ )
+ .catch((error: unknown) => {
+ if (error instanceof WaitForTransactionReceiptTimeoutError) {
+ startSpan(
+ { name: "nonce reset", op: "tx.reset", attributes: { "tx.nonce": prepared.nonce } },
+ (resetSpan) => {
+ const info = nonceManager.info({ address: client.account.address, chainId: chain.id });
+ resetSpan.setAttribute("exa.reset", true);
+ resetSpan.setAttribute("exa.delta", info.delta);
+ resetSpan.setAttribute("exa.nonce", info.nonce);
+ nonceManager.hardReset({ address: client.account.address, chainId: chain.id });
+ },
+ );
+ }
+ throw error;
+ })
+ .finally(() => {
+ abortController.abort();
+ }),
+ Promise.resolve(options?.onHash?.(hash)).catch((error: unknown) =>
+ captureException(error, { level: "error" }),
+ ),
+ ]);
+ if (receiptResult.status === "rejected") throw receiptResult.reason;
+ const receipt = receiptResult.value;
+ scope.setContext("tx", { request, receipt });
+ const [trace] = await Promise.all([
+ startSpan({ name: "trace transaction", op: "tx.trace" }, () =>
+ withRetry(() => traceClient.traceTransaction(hash), {
+ delay: 1000,
+ retryCount: 10,
+ shouldRetry: ({ error }) => error instanceof InvalidInputRpcError,
+ }).catch((error: unknown) => {
+ captureException(error, { level: "error" });
+ return null;
+ }),
+ ),
+ Promise.resolve(options?.onReceipt?.(receipt)).catch((error: unknown) =>
+ captureException(error, { level: "error" }),
+ ),
+ ]);
+ scope.setContext("tx", { request, receipt, trace });
+ if (receipt.status !== "success") {
+ if (!trace) throw new Error("no trace");
+ // eslint-disable-next-line @typescript-eslint/only-throw-error -- returns error
+ throw getContractError(new RawContractError({ data: trace.output }), { ...call, args: call.args ?? [] });
+ }
+ span.setStatus({ code: SPAN_STATUS_OK });
+ return receipt;
+ } catch (error: unknown) {
+ const reason = revertReason(error, { fallback: "message", withArguments: true });
+ if (options?.ignore) {
+ const ignore =
+ typeof options.ignore === "function" ? await options.ignore(reason) : options.ignore.includes(reason);
+ if (ignore) {
+ span.setAttribute("exa.error", reason);
+ span.setStatus({ code: SPAN_STATUS_OK });
+ return ignore === true ? null : ignore;
+ }
+ }
+ span.setStatus({ code: SPAN_STATUS_ERROR, message: reason });
+ const level =
+ typeof options?.level === "function" ? options.level(reason, error) : (options?.level ?? "error");
+ if (level) {
+ withScope((captureScope) => {
+ const fingerprint = revertFingerprint(error);
+ if (fingerprint[1] && fingerprint[1] !== "unknown") {
+ const type = fingerprint.length > 2 ? `${fingerprint[1]}(${fingerprint[2]})` : fingerprint[1];
+ captureScope.addEventProcessor((event) => {
+ if (event.exception?.values?.[0]) event.exception.values[0].type = type;
+ return event;
+ });
+ }
+ captureException(error, { level, fingerprint });
+ });
+ }
+ throw error;
+ }
+ }),
+ ),
+ };
+}
+
+export async function getAccount(): Promise {
+ await initializeGcpCredentials();
+
+ if (!(await hasCredentials())) {
+ throw new Error(
+ `gcp credentials file not found at ${GOOGLE_APPLICATION_CREDENTIALS}. ` +
+ `ensure GCP_BASE64_JSON environment variable is set.`,
+ );
+ }
+
+ try {
+ const account = await withRetry(
+ () =>
+ gcpHsmToAccount({
+ hsmKeyVersion: `projects/${projectId}/locations/us-west2/keyRings/${keyRing}/cryptoKeys/allower/cryptoKeyVersions/${version}`,
+ kmsClient: new KeyManagementServiceClient({
+ keyFilename: GOOGLE_APPLICATION_CREDENTIALS,
+ }),
+ }),
+ {
+ delay: 2000,
+ retryCount: 3,
+ shouldRetry: ({ error }) => isRetryableKmsError(error),
+ },
+ );
+
+ account.nonceManager = nonceManager;
+ return account;
+ } catch (error: unknown) {
+ captureException(error, { level: "error" });
+ throw error;
+ }
+}
+
+export async function allower() {
+ return createWalletClient({
+ chain,
+ transport: http(`${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`, {
+ batch: true,
+ async onFetchRequest(request) {
+ try {
+ captureRequests(parse(Requests, await request.clone().json()));
+ } catch (error: unknown) {
+ captureMessage("failed to parse or capture rpc requests", {
+ level: "error",
+ extra: { error },
+ });
+ }
+ },
+ }),
+ account: await getAccount(),
+ }).extend((client: WalletClient) => {
+ const send = withExaSend(client);
+ return {
+ ...send,
+ allow: async (account: Address, options?: { ignore?: string[] }) => {
+ if (!firewallAddress) throw new Error("firewall address not configured");
+ return send.exaSend(
+ { forceTransaction: true, name: "firewall.allow", op: "exa.firewall", attributes: { account } },
+ {
+ address: firewallAddress,
+ functionName: "allow",
+ args: [account, true],
+ abi: firewallAbi,
+ },
+ options?.ignore ? { ignore: options.ignore } : undefined,
+ );
+ },
+ };
+ });
+}
diff --git a/server/utils/gcp.ts b/server/utils/gcp.ts
new file mode 100644
index 000000000..cadfbde95
--- /dev/null
+++ b/server/utils/gcp.ts
@@ -0,0 +1,72 @@
+import { access, writeFile } from "node:fs/promises";
+import { number, object, safeParse, string } from "valibot";
+
+const DECODING_ITERATIONS = 3;
+export const GOOGLE_APPLICATION_CREDENTIALS = "/tmp/gcp-service-account.json";
+
+if (!process.env.GCP_BASE64_JSON) throw new Error("GCP_BASE64_JSON is required when using GCP KMS");
+const gcpBase64Json = process.env.GCP_BASE64_JSON;
+
+let initializationPromise: null | Promise = null;
+
+export function resetGcpInitialization() {
+ initializationPromise = null;
+}
+
+export async function initializeGcpCredentials() {
+ if (initializationPromise) {
+ return initializationPromise;
+ }
+
+ initializationPromise = (async () => {
+ if (await hasCredentials()) {
+ return;
+ }
+
+ let json = gcpBase64Json;
+ for (let index = 0; index < DECODING_ITERATIONS; index++) {
+ json = Buffer.from(json, "base64").toString("utf8");
+ }
+ await writeFile(GOOGLE_APPLICATION_CREDENTIALS, json, { mode: 0o600 });
+ })().catch((error: unknown) => {
+ initializationPromise = null;
+ throw error;
+ });
+
+ return initializationPromise;
+}
+
+export async function hasCredentials(): Promise {
+ return access(GOOGLE_APPLICATION_CREDENTIALS)
+ .then(() => true)
+ .catch(() => false);
+}
+
+export function isRetryableKmsError(error: unknown): boolean {
+ if (!(error instanceof Error)) return false;
+
+ const numericResult = safeParse(object({ code: number() }), error);
+ if (numericResult.success) {
+ const code = numericResult.output.code;
+ return code === 14 || code === 4 || code === 13 || code === 8;
+ }
+
+ const stringResult = safeParse(object({ code: string() }), error);
+ if (stringResult.success) {
+ const code = stringResult.output.code;
+ return (
+ code === "UNAVAILABLE" || code === "DEADLINE_EXCEEDED" || code === "INTERNAL" || code === "RESOURCE_EXHAUSTED"
+ );
+ }
+
+ const message = error.message.toLowerCase();
+ return (
+ message.includes("network") ||
+ message.includes("timeout") ||
+ message.includes("unavailable") ||
+ message.includes("internal error") ||
+ message.includes("service unavailable") ||
+ error.name === "NetworkError" ||
+ error.name === "TimeoutError"
+ );
+}
diff --git a/server/utils/keeper.ts b/server/utils/keeper.ts
deleted file mode 100644
index 0b767c0d7..000000000
--- a/server/utils/keeper.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from "@sentry/core";
-import { captureException, startSpan, withScope } from "@sentry/node";
-import { setTimeout } from "node:timers/promises";
-import { parse, safeParse } from "valibot";
-import {
- createWalletClient,
- encodeFunctionData,
- getContractError,
- http,
- InvalidInputRpcError,
- keccak256,
- RawContractError,
- WaitForTransactionReceiptTimeoutError,
- withRetry,
- type HttpTransport,
- type MaybePromise,
- type Prettify,
- type PrivateKeyAccount,
- type TransactionReceipt,
- type WalletClient,
- type WriteContractParameters,
-} from "viem";
-import { privateKeyToAccount } from "viem/accounts";
-
-import alchemyAPIKey from "@exactly/common/alchemyAPIKey";
-import chain from "@exactly/common/generated/chain";
-import revertReason from "@exactly/common/revertReason";
-import { Address, Hash } from "@exactly/common/validation";
-
-import nonceManager from "./nonceManager";
-import publicClient, { captureRequests, Requests } from "./publicClient";
-import revertFingerprint from "./revertFingerprint";
-import traceClient from "./traceClient";
-
-if (!chain.rpcUrls.alchemy.http[0]) throw new Error("missing alchemy rpc url");
-
-export default createWalletClient({
- chain,
- transport: http(`${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`, {
- batch: true,
- async onFetchRequest(request) {
- captureRequests(parse(Requests, await request.json()));
- },
- }),
- account: privateKeyToAccount(
- parse(Hash, process.env.KEEPER_PRIVATE_KEY, {
- message: "invalid keeper private key",
- }),
- { nonceManager },
- ),
-}).extend(extender);
-
-export function extender(keeper: WalletClient) {
- return {
- exaSend: async (
- spanOptions: Prettify[0], "name" | "op"> & { name: string; op: string }>,
- call: Prettify>,
- options?: {
- ignore?: ((reason: string) => MaybePromise) | string[];
- level?: "error" | "warning" | ((reason: string, error: unknown) => "error" | "warning" | false) | false;
- onHash?: (hash: Hash) => MaybePromise;
- },
- ) =>
- withScope((scope) =>
- startSpan({ forceTransaction: true, ...spanOptions }, async (span) => {
- const account = safeParse(Address, spanOptions.attributes?.account);
- if (account.success) scope.setUser({ id: account.output });
- try {
- scope.setContext("tx", { call });
- span.setAttributes({
- "tx.call": `${call.functionName}(${call.args?.map(String).join(", ") ?? ""})`,
- "tx.from": keeper.account.address,
- "tx.to": call.address,
- });
- const txOptions = {
- type: "eip1559",
- maxFeePerGas: 1_000_000_000n,
- maxPriorityFeePerGas: 1_000_000n,
- gas: 5_000_000n,
- } as const;
- const { request: writeRequest } = await startSpan({ name: "eth_call", op: "tx.simulate" }, () =>
- publicClient.simulateContract({ account: keeper.account, ...txOptions, ...call }),
- );
- const {
- abi: _,
- account: __,
- address: ___,
- ...request
- } = { from: writeRequest.account.address, to: writeRequest.address, ...writeRequest };
- scope.setContext("tx", { request });
- const prepared = await startSpan({ name: "prepare transaction", op: "tx.prepare" }, () =>
- keeper.prepareTransactionRequest({
- to: call.address,
- data: encodeFunctionData(call),
- ...txOptions,
- nonceManager,
- }),
- );
- scope.setContext("tx", { request, prepared });
- span.setAttribute("tx.nonce", prepared.nonce);
- const serializedTransaction = await startSpan({ name: "sign transaction", op: "tx.sign" }, () =>
- keeper.signTransaction(prepared),
- );
- const hash = keccak256(serializedTransaction);
- scope.setContext("tx", { request, prepared, hash });
- span.setAttribute("tx.hash", hash);
- const abortController = new AbortController();
- const [, receiptResult] = await Promise.allSettled([
- (async () => {
- while (!abortController.signal.aborted) {
- await Promise.allSettled([
- startSpan({ name: "send transaction", op: "tx.send" }, () =>
- publicClient.sendRawTransaction({ serializedTransaction }),
- ).catch((error: unknown) => {
- captureException(error, { level: "error" });
- throw error;
- }),
- setTimeout(10_000, null, { signal: abortController.signal }),
- ]);
- }
- })(),
- startSpan({ name: "wait for receipt", op: "tx.wait" }, () =>
- publicClient.waitForTransactionReceipt({ hash, confirmations: 0 }),
- )
- .catch((error: unknown) => {
- if (error instanceof WaitForTransactionReceiptTimeoutError) {
- startSpan(
- { name: "nonce reset", op: "tx.reset", attributes: { "tx.nonce": prepared.nonce } },
- (resetSpan) => {
- const info = nonceManager.info({ address: keeper.account.address, chainId: chain.id });
- resetSpan.setAttribute("exa.reset", true);
- resetSpan.setAttribute("exa.delta", info.delta);
- resetSpan.setAttribute("exa.nonce", info.nonce);
- nonceManager.hardReset({ address: keeper.account.address, chainId: chain.id });
- },
- );
- }
- throw error;
- })
- .finally(() => {
- abortController.abort();
- }),
- Promise.resolve(options?.onHash?.(hash)).catch((error: unknown) =>
- captureException(error, { level: "error" }),
- ),
- ]);
- if (receiptResult.status === "rejected") throw receiptResult.reason;
- const receipt = receiptResult.value;
- scope.setContext("tx", { request, receipt });
- const trace = await startSpan({ name: "trace transaction", op: "tx.trace" }, () =>
- withRetry(() => traceClient.traceTransaction(hash), {
- delay: 1000,
- retryCount: 10,
- shouldRetry: ({ error }) => error instanceof InvalidInputRpcError,
- }).catch((error: unknown) => {
- captureException(error, { level: "error" });
- return null;
- }),
- );
- scope.setContext("tx", { request, receipt, trace });
- if (receipt.status !== "success") {
- if (!trace) throw new Error("no trace");
- // eslint-disable-next-line @typescript-eslint/only-throw-error -- returns error
- throw getContractError(new RawContractError({ data: trace.output }), { ...call, args: call.args ?? [] });
- }
- span.setStatus({ code: SPAN_STATUS_OK });
- return receipt;
- } catch (error: unknown) {
- const reason = revertReason(error, { fallback: "message", withArguments: true });
- if (options?.ignore) {
- const ignore =
- typeof options.ignore === "function" ? await options.ignore(reason) : options.ignore.includes(reason);
- if (ignore) {
- span.setAttribute("exa.error", reason);
- span.setStatus({ code: SPAN_STATUS_OK });
- return ignore === true ? null : ignore;
- }
- }
- span.setStatus({ code: SPAN_STATUS_ERROR, message: reason });
- const level =
- typeof options?.level === "function" ? options.level(reason, error) : (options?.level ?? "error");
- if (level) {
- withScope((captureScope) => {
- const fingerprint = revertFingerprint(error);
- if (fingerprint[1] && fingerprint[1] !== "unknown") {
- const type = fingerprint.length > 2 ? `${fingerprint[1]}(${fingerprint[2]})` : fingerprint[1];
- captureScope.addEventProcessor((event) => {
- if (event.exception?.values?.[0]) event.exception.values[0].type = type;
- return event;
- });
- }
- captureException(error, { level, fingerprint });
- });
- }
- throw error;
- }
- }),
- ),
- };
-}
diff --git a/server/vitest.config.mts b/server/vitest.config.mts
index bca929b42..b62b8872a 100644
--- a/server/vitest.config.mts
+++ b/server/vitest.config.mts
@@ -19,6 +19,10 @@ export default defineConfig({
BRIDGE_API_KEY: "bridge",
BRIDGE_API_URL: "https://bridge.test",
EXPO_PUBLIC_ALCHEMY_API_KEY: " ",
+ GCP_BASE64_JSON: "WlhsS01HVllRbXhKYW05blNXNU9iR051V25CWk1sWm1XVmRPYW1JelZuVmtRMG81UTJjOVBRbz0K",
+ GCP_KMS_KEY_RING: "op-sepolia",
+ GCP_KMS_KEY_VERSION: "1",
+ GCP_PROJECT_ID: "exa-dev",
INTERCOM_IDENTITY_KEY: "a9cBeTfEtGPSQ58REZP35Bx00ofajvStEc8TTuBtSmk",
ISSUER_PRIVATE_KEY: padHex("0x420"),
MANTECA_API_URL: "https://manteca.test",