@unionlabs/payments 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. package/Abi/package.json +6 -0
  2. package/Attestor/package.json +6 -0
  3. package/Constants/package.json +6 -0
  4. package/Domain/package.json +6 -0
  5. package/Error/package.json +6 -0
  6. package/EvmPublicClient/package.json +6 -0
  7. package/EvmWalletClient/package.json +6 -0
  8. package/Payment/package.json +6 -0
  9. package/PaymentError/package.json +6 -0
  10. package/Prover/package.json +6 -0
  11. package/PublicClient/package.json +6 -0
  12. package/README.md +1 -0
  13. package/Schema/package.json +6 -0
  14. package/Utils/package.json +6 -0
  15. package/WalletClient/package.json +6 -0
  16. package/attestation/package.json +6 -0
  17. package/cli/commands/balance/package.json +6 -0
  18. package/cli/commands/deposit/package.json +6 -0
  19. package/cli/commands/export-verifier/package.json +6 -0
  20. package/cli/commands/generate/package.json +6 -0
  21. package/cli/commands/history/package.json +6 -0
  22. package/cli/commands/prove/package.json +6 -0
  23. package/cli/commands/redeem/package.json +6 -0
  24. package/cli/commands/update-client/package.json +6 -0
  25. package/cli/config/package.json +6 -0
  26. package/cli/package.json +6 -0
  27. package/cli/utils/package.json +6 -0
  28. package/constants/ibc-core-registry/package.json +6 -0
  29. package/constants/services/package.json +6 -0
  30. package/constants/z-asset-registry/package.json +6 -0
  31. package/dist/cjs/Abi.js +270 -0
  32. package/dist/cjs/Abi.js.map +1 -0
  33. package/dist/cjs/Attestor.js +76 -0
  34. package/dist/cjs/Attestor.js.map +1 -0
  35. package/dist/cjs/Constants.js +8 -0
  36. package/dist/cjs/Constants.js.map +1 -0
  37. package/dist/cjs/Domain.js +24 -0
  38. package/dist/cjs/Domain.js.map +1 -0
  39. package/dist/cjs/Error.js +100 -0
  40. package/dist/cjs/Error.js.map +1 -0
  41. package/dist/cjs/EvmPublicClient.js +301 -0
  42. package/dist/cjs/EvmPublicClient.js.map +1 -0
  43. package/dist/cjs/EvmWalletClient.js +670 -0
  44. package/dist/cjs/EvmWalletClient.js.map +1 -0
  45. package/dist/cjs/Payment.js +333 -0
  46. package/dist/cjs/Payment.js.map +1 -0
  47. package/dist/cjs/PaymentError.js +32 -0
  48. package/dist/cjs/PaymentError.js.map +1 -0
  49. package/dist/cjs/Prover.js +153 -0
  50. package/dist/cjs/Prover.js.map +1 -0
  51. package/dist/cjs/PublicClient.js +39 -0
  52. package/dist/cjs/PublicClient.js.map +1 -0
  53. package/dist/cjs/Schema.js +38 -0
  54. package/dist/cjs/Schema.js.map +1 -0
  55. package/dist/cjs/Utils.js +33 -0
  56. package/dist/cjs/Utils.js.map +1 -0
  57. package/dist/cjs/WalletClient.js +39 -0
  58. package/dist/cjs/WalletClient.js.map +1 -0
  59. package/dist/cjs/attestation.js +49 -0
  60. package/dist/cjs/attestation.js.map +1 -0
  61. package/dist/cjs/cli/commands/balance.js +60 -0
  62. package/dist/cjs/cli/commands/balance.js.map +1 -0
  63. package/dist/cjs/cli/commands/deposit.js +58 -0
  64. package/dist/cjs/cli/commands/deposit.js.map +1 -0
  65. package/dist/cjs/cli/commands/export-verifier.js +27 -0
  66. package/dist/cjs/cli/commands/export-verifier.js.map +1 -0
  67. package/dist/cjs/cli/commands/generate.js +41 -0
  68. package/dist/cjs/cli/commands/generate.js.map +1 -0
  69. package/dist/cjs/cli/commands/history.js +59 -0
  70. package/dist/cjs/cli/commands/history.js.map +1 -0
  71. package/dist/cjs/cli/commands/prove.js +82 -0
  72. package/dist/cjs/cli/commands/prove.js.map +1 -0
  73. package/dist/cjs/cli/commands/redeem.js +152 -0
  74. package/dist/cjs/cli/commands/redeem.js.map +1 -0
  75. package/dist/cjs/cli/commands/update-client.js +62 -0
  76. package/dist/cjs/cli/commands/update-client.js.map +1 -0
  77. package/dist/cjs/cli/config.js +32 -0
  78. package/dist/cjs/cli/config.js.map +1 -0
  79. package/dist/cjs/cli/utils.js +108 -0
  80. package/dist/cjs/cli/utils.js.map +1 -0
  81. package/dist/cjs/cli.js +24 -0
  82. package/dist/cjs/cli.js.map +1 -0
  83. package/dist/cjs/constants/ibc-core-registry.js +30 -0
  84. package/dist/cjs/constants/ibc-core-registry.js.map +1 -0
  85. package/dist/cjs/constants/services.js +9 -0
  86. package/dist/cjs/constants/services.js.map +1 -0
  87. package/dist/cjs/constants/z-asset-registry.js +32 -0
  88. package/dist/cjs/constants/z-asset-registry.js.map +1 -0
  89. package/dist/cjs/gen/prover_pb.js +81 -0
  90. package/dist/cjs/gen/prover_pb.js.map +1 -0
  91. package/dist/cjs/index.js +32 -0
  92. package/dist/cjs/index.js.map +1 -0
  93. package/dist/cjs/internal/evm.js +191 -0
  94. package/dist/cjs/internal/evm.js.map +1 -0
  95. package/dist/cjs/internal/evmWalletClient.js +6 -0
  96. package/dist/cjs/internal/evmWalletClient.js.map +1 -0
  97. package/dist/cjs/internal/publicClient.js +49 -0
  98. package/dist/cjs/internal/publicClient.js.map +1 -0
  99. package/dist/cjs/internal/walletClient.js +43 -0
  100. package/dist/cjs/internal/walletClient.js.map +1 -0
  101. package/dist/cjs/legacy/client.js +1222 -0
  102. package/dist/cjs/legacy/client.js.map +1 -0
  103. package/dist/cjs/legacy/prover.js +112 -0
  104. package/dist/cjs/legacy/prover.js.map +1 -0
  105. package/dist/cjs/poseidon2.js +226 -0
  106. package/dist/cjs/poseidon2.js.map +1 -0
  107. package/dist/cjs/promises/Attestor.js +23 -0
  108. package/dist/cjs/promises/Attestor.js.map +1 -0
  109. package/dist/cjs/promises/EvmPublicClient.js +34 -0
  110. package/dist/cjs/promises/EvmPublicClient.js.map +1 -0
  111. package/dist/cjs/promises/EvmWalletClient.js +51 -0
  112. package/dist/cjs/promises/EvmWalletClient.js.map +1 -0
  113. package/dist/cjs/promises/Payment.js +22 -0
  114. package/dist/cjs/promises/Payment.js.map +1 -0
  115. package/dist/cjs/promises/Prover.js +26 -0
  116. package/dist/cjs/promises/Prover.js.map +1 -0
  117. package/dist/cjs/promises/PublicClient.js +53 -0
  118. package/dist/cjs/promises/PublicClient.js.map +1 -0
  119. package/dist/cjs/promises/WalletClient.js +44 -0
  120. package/dist/cjs/promises/WalletClient.js.map +1 -0
  121. package/dist/cjs/promises/index.js +22 -0
  122. package/dist/cjs/promises/index.js.map +1 -0
  123. package/dist/cjs/rpc.js +867 -0
  124. package/dist/cjs/rpc.js.map +1 -0
  125. package/dist/cjs/types.js +6 -0
  126. package/dist/cjs/types.js.map +1 -0
  127. package/dist/dts/Abi.d.ts +220 -0
  128. package/dist/dts/Abi.d.ts.map +1 -0
  129. package/dist/dts/Attestor.d.ts +42 -0
  130. package/dist/dts/Attestor.d.ts.map +1 -0
  131. package/dist/dts/Constants.d.ts +3 -0
  132. package/dist/dts/Constants.d.ts.map +1 -0
  133. package/dist/dts/Domain.d.ts +141 -0
  134. package/dist/dts/Domain.d.ts.map +1 -0
  135. package/dist/dts/Error.d.ts +102 -0
  136. package/dist/dts/Error.d.ts.map +1 -0
  137. package/dist/dts/EvmPublicClient.d.ts +61 -0
  138. package/dist/dts/EvmPublicClient.d.ts.map +1 -0
  139. package/dist/dts/EvmWalletClient.d.ts +67 -0
  140. package/dist/dts/EvmWalletClient.d.ts.map +1 -0
  141. package/dist/dts/Payment.d.ts +128 -0
  142. package/dist/dts/Payment.d.ts.map +1 -0
  143. package/dist/dts/PaymentError.d.ts +21 -0
  144. package/dist/dts/PaymentError.d.ts.map +1 -0
  145. package/dist/dts/Prover.d.ts +87 -0
  146. package/dist/dts/Prover.d.ts.map +1 -0
  147. package/dist/dts/PublicClient.d.ts +146 -0
  148. package/dist/dts/PublicClient.d.ts.map +1 -0
  149. package/dist/dts/Schema.d.ts +16 -0
  150. package/dist/dts/Schema.d.ts.map +1 -0
  151. package/dist/dts/Utils.d.ts +11 -0
  152. package/dist/dts/Utils.d.ts.map +1 -0
  153. package/dist/dts/WalletClient.d.ts +123 -0
  154. package/dist/dts/WalletClient.d.ts.map +1 -0
  155. package/dist/dts/attestation.d.ts +13 -0
  156. package/dist/dts/attestation.d.ts.map +1 -0
  157. package/dist/dts/cli/commands/balance.d.ts +3 -0
  158. package/dist/dts/cli/commands/balance.d.ts.map +1 -0
  159. package/dist/dts/cli/commands/deposit.d.ts +3 -0
  160. package/dist/dts/cli/commands/deposit.d.ts.map +1 -0
  161. package/dist/dts/cli/commands/export-verifier.d.ts +3 -0
  162. package/dist/dts/cli/commands/export-verifier.d.ts.map +1 -0
  163. package/dist/dts/cli/commands/generate.d.ts +3 -0
  164. package/dist/dts/cli/commands/generate.d.ts.map +1 -0
  165. package/dist/dts/cli/commands/history.d.ts +3 -0
  166. package/dist/dts/cli/commands/history.d.ts.map +1 -0
  167. package/dist/dts/cli/commands/prove.d.ts +3 -0
  168. package/dist/dts/cli/commands/prove.d.ts.map +1 -0
  169. package/dist/dts/cli/commands/redeem.d.ts +3 -0
  170. package/dist/dts/cli/commands/redeem.d.ts.map +1 -0
  171. package/dist/dts/cli/commands/update-client.d.ts +3 -0
  172. package/dist/dts/cli/commands/update-client.d.ts.map +1 -0
  173. package/dist/dts/cli/config.d.ts +14 -0
  174. package/dist/dts/cli/config.d.ts.map +1 -0
  175. package/dist/dts/cli/utils.d.ts +11 -0
  176. package/dist/dts/cli/utils.d.ts.map +1 -0
  177. package/dist/dts/cli.d.ts +3 -0
  178. package/dist/dts/cli.d.ts.map +1 -0
  179. package/dist/dts/constants/ibc-core-registry.d.ts +11 -0
  180. package/dist/dts/constants/ibc-core-registry.d.ts.map +1 -0
  181. package/dist/dts/constants/services.d.ts +3 -0
  182. package/dist/dts/constants/services.d.ts.map +1 -0
  183. package/dist/dts/constants/z-asset-registry.d.ts +13 -0
  184. package/dist/dts/constants/z-asset-registry.d.ts.map +1 -0
  185. package/dist/dts/gen/prover_pb.d.ts +300 -0
  186. package/dist/dts/gen/prover_pb.d.ts.map +1 -0
  187. package/dist/dts/index.d.ts +21 -0
  188. package/dist/dts/index.d.ts.map +1 -0
  189. package/dist/dts/internal/evm.d.ts +250 -0
  190. package/dist/dts/internal/evm.d.ts.map +1 -0
  191. package/dist/dts/internal/evmWalletClient.d.ts +2 -0
  192. package/dist/dts/internal/evmWalletClient.d.ts.map +1 -0
  193. package/dist/dts/internal/publicClient.d.ts +2 -0
  194. package/dist/dts/internal/publicClient.d.ts.map +1 -0
  195. package/dist/dts/internal/walletClient.d.ts +2 -0
  196. package/dist/dts/internal/walletClient.d.ts.map +1 -0
  197. package/dist/dts/legacy/client.d.ts +313 -0
  198. package/dist/dts/legacy/client.d.ts.map +1 -0
  199. package/dist/dts/legacy/prover.d.ts +30 -0
  200. package/dist/dts/legacy/prover.d.ts.map +1 -0
  201. package/dist/dts/poseidon2.d.ts +18 -0
  202. package/dist/dts/poseidon2.d.ts.map +1 -0
  203. package/dist/dts/promises/Attestor.d.ts +17 -0
  204. package/dist/dts/promises/Attestor.d.ts.map +1 -0
  205. package/dist/dts/promises/EvmPublicClient.d.ts +3709 -0
  206. package/dist/dts/promises/EvmPublicClient.d.ts.map +1 -0
  207. package/dist/dts/promises/EvmWalletClient.d.ts +4502 -0
  208. package/dist/dts/promises/EvmWalletClient.d.ts.map +1 -0
  209. package/dist/dts/promises/Payment.d.ts +33 -0
  210. package/dist/dts/promises/Payment.d.ts.map +1 -0
  211. package/dist/dts/promises/Prover.d.ts +14 -0
  212. package/dist/dts/promises/Prover.d.ts.map +1 -0
  213. package/dist/dts/promises/PublicClient.d.ts +23 -0
  214. package/dist/dts/promises/PublicClient.d.ts.map +1 -0
  215. package/dist/dts/promises/WalletClient.d.ts +57 -0
  216. package/dist/dts/promises/WalletClient.d.ts.map +1 -0
  217. package/dist/dts/promises/index.d.ts +8 -0
  218. package/dist/dts/promises/index.d.ts.map +1 -0
  219. package/dist/dts/rpc.d.ts +148 -0
  220. package/dist/dts/rpc.d.ts.map +1 -0
  221. package/dist/dts/types.d.ts +263 -0
  222. package/dist/dts/types.d.ts.map +1 -0
  223. package/dist/esm/Abi.js +264 -0
  224. package/dist/esm/Abi.js.map +1 -0
  225. package/dist/esm/Attestor.js +68 -0
  226. package/dist/esm/Attestor.js.map +1 -0
  227. package/dist/esm/Constants.js +2 -0
  228. package/dist/esm/Constants.js.map +1 -0
  229. package/dist/esm/Domain.js +17 -0
  230. package/dist/esm/Domain.js.map +1 -0
  231. package/dist/esm/Error.js +89 -0
  232. package/dist/esm/Error.js.map +1 -0
  233. package/dist/esm/EvmPublicClient.js +292 -0
  234. package/dist/esm/EvmPublicClient.js.map +1 -0
  235. package/dist/esm/EvmWalletClient.js +659 -0
  236. package/dist/esm/EvmWalletClient.js.map +1 -0
  237. package/dist/esm/Payment.js +323 -0
  238. package/dist/esm/Payment.js.map +1 -0
  239. package/dist/esm/PaymentError.js +24 -0
  240. package/dist/esm/PaymentError.js.map +1 -0
  241. package/dist/esm/Prover.js +142 -0
  242. package/dist/esm/Prover.js.map +1 -0
  243. package/dist/esm/PublicClient.js +30 -0
  244. package/dist/esm/PublicClient.js.map +1 -0
  245. package/dist/esm/Schema.js +31 -0
  246. package/dist/esm/Schema.js.map +1 -0
  247. package/dist/esm/Utils.js +27 -0
  248. package/dist/esm/Utils.js.map +1 -0
  249. package/dist/esm/WalletClient.js +30 -0
  250. package/dist/esm/WalletClient.js.map +1 -0
  251. package/dist/esm/attestation.js +42 -0
  252. package/dist/esm/attestation.js.map +1 -0
  253. package/dist/esm/cli/commands/balance.js +54 -0
  254. package/dist/esm/cli/commands/balance.js.map +1 -0
  255. package/dist/esm/cli/commands/deposit.js +52 -0
  256. package/dist/esm/cli/commands/deposit.js.map +1 -0
  257. package/dist/esm/cli/commands/export-verifier.js +21 -0
  258. package/dist/esm/cli/commands/export-verifier.js.map +1 -0
  259. package/dist/esm/cli/commands/generate.js +35 -0
  260. package/dist/esm/cli/commands/generate.js.map +1 -0
  261. package/dist/esm/cli/commands/history.js +53 -0
  262. package/dist/esm/cli/commands/history.js.map +1 -0
  263. package/dist/esm/cli/commands/prove.js +76 -0
  264. package/dist/esm/cli/commands/prove.js.map +1 -0
  265. package/dist/esm/cli/commands/redeem.js +146 -0
  266. package/dist/esm/cli/commands/redeem.js.map +1 -0
  267. package/dist/esm/cli/commands/update-client.js +56 -0
  268. package/dist/esm/cli/commands/update-client.js.map +1 -0
  269. package/dist/esm/cli/config.js +26 -0
  270. package/dist/esm/cli/config.js.map +1 -0
  271. package/dist/esm/cli/utils.js +94 -0
  272. package/dist/esm/cli/utils.js.map +1 -0
  273. package/dist/esm/cli.js +22 -0
  274. package/dist/esm/cli.js.map +1 -0
  275. package/dist/esm/constants/ibc-core-registry.js +21 -0
  276. package/dist/esm/constants/ibc-core-registry.js.map +1 -0
  277. package/dist/esm/constants/services.js +3 -0
  278. package/dist/esm/constants/services.js.map +1 -0
  279. package/dist/esm/constants/z-asset-registry.js +23 -0
  280. package/dist/esm/constants/z-asset-registry.js.map +1 -0
  281. package/dist/esm/gen/prover_pb.js +74 -0
  282. package/dist/esm/gen/prover_pb.js.map +1 -0
  283. package/dist/esm/index.js +22 -0
  284. package/dist/esm/index.js.map +1 -0
  285. package/dist/esm/internal/evm.js +169 -0
  286. package/dist/esm/internal/evm.js.map +1 -0
  287. package/dist/esm/internal/evmWalletClient.js +2 -0
  288. package/dist/esm/internal/evmWalletClient.js.map +1 -0
  289. package/dist/esm/internal/publicClient.js +41 -0
  290. package/dist/esm/internal/publicClient.js.map +1 -0
  291. package/dist/esm/internal/walletClient.js +35 -0
  292. package/dist/esm/internal/walletClient.js.map +1 -0
  293. package/dist/esm/legacy/client.js +1212 -0
  294. package/dist/esm/legacy/client.js.map +1 -0
  295. package/dist/esm/legacy/prover.js +105 -0
  296. package/dist/esm/legacy/prover.js.map +1 -0
  297. package/dist/esm/package.json +4 -0
  298. package/dist/esm/poseidon2.js +218 -0
  299. package/dist/esm/poseidon2.js.map +1 -0
  300. package/dist/esm/promises/Attestor.js +14 -0
  301. package/dist/esm/promises/Attestor.js.map +1 -0
  302. package/dist/esm/promises/EvmPublicClient.js +26 -0
  303. package/dist/esm/promises/EvmPublicClient.js.map +1 -0
  304. package/dist/esm/promises/EvmWalletClient.js +43 -0
  305. package/dist/esm/promises/EvmWalletClient.js.map +1 -0
  306. package/dist/esm/promises/Payment.js +13 -0
  307. package/dist/esm/promises/Payment.js.map +1 -0
  308. package/dist/esm/promises/Prover.js +17 -0
  309. package/dist/esm/promises/Prover.js.map +1 -0
  310. package/dist/esm/promises/PublicClient.js +45 -0
  311. package/dist/esm/promises/PublicClient.js.map +1 -0
  312. package/dist/esm/promises/WalletClient.js +36 -0
  313. package/dist/esm/promises/WalletClient.js.map +1 -0
  314. package/dist/esm/promises/index.js +8 -0
  315. package/dist/esm/promises/index.js.map +1 -0
  316. package/dist/esm/rpc.js +850 -0
  317. package/dist/esm/rpc.js.map +1 -0
  318. package/dist/esm/types.js +2 -0
  319. package/dist/esm/types.js.map +1 -0
  320. package/gen/prover_pb/package.json +6 -0
  321. package/index/package.json +6 -0
  322. package/legacy/client/package.json +6 -0
  323. package/legacy/prover/package.json +6 -0
  324. package/package.json +397 -44
  325. package/poseidon2/package.json +6 -0
  326. package/promises/Attestor/package.json +6 -0
  327. package/promises/EvmPublicClient/package.json +6 -0
  328. package/promises/EvmWalletClient/package.json +6 -0
  329. package/promises/Payment/package.json +6 -0
  330. package/promises/Prover/package.json +6 -0
  331. package/promises/PublicClient/package.json +6 -0
  332. package/promises/WalletClient/package.json +6 -0
  333. package/promises/index/package.json +6 -0
  334. package/promises/package.json +6 -0
  335. package/rpc/package.json +6 -0
  336. package/src/Abi.ts +195 -0
  337. package/src/Attestor.ts +113 -0
  338. package/src/Constants.ts +4 -0
  339. package/src/Domain.ts +52 -0
  340. package/src/Error.ts +163 -0
  341. package/src/EvmPublicClient.ts +549 -0
  342. package/src/EvmWalletClient.ts +1034 -0
  343. package/src/Payment.ts +523 -0
  344. package/src/PaymentError.ts +39 -0
  345. package/src/Prover.ts +240 -0
  346. package/src/PublicClient.ts +196 -0
  347. package/src/Schema.ts +36 -0
  348. package/src/Utils.ts +43 -0
  349. package/src/WalletClient.ts +172 -0
  350. package/src/attestation.ts +69 -0
  351. package/src/cli/commands/balance.ts +88 -0
  352. package/src/cli/commands/deposit.ts +104 -0
  353. package/src/cli/commands/export-verifier.ts +28 -0
  354. package/src/cli/commands/generate.ts +86 -0
  355. package/src/cli/commands/history.ts +91 -0
  356. package/src/cli/commands/prove.ts +133 -0
  357. package/src/cli/commands/redeem.ts +277 -0
  358. package/src/cli/commands/update-client.ts +96 -0
  359. package/src/cli/config.ts +55 -0
  360. package/src/cli/utils.ts +136 -0
  361. package/src/cli.ts +31 -0
  362. package/src/constants/ibc-core-registry.ts +44 -0
  363. package/src/constants/services.ts +4 -0
  364. package/src/constants/z-asset-registry.ts +47 -0
  365. package/src/gen/prover_pb.ts +375 -0
  366. package/src/index.ts +23 -0
  367. package/src/internal/evm.ts +361 -0
  368. package/src/internal/evmWalletClient.ts +0 -0
  369. package/src/internal/publicClient.ts +57 -0
  370. package/src/internal/walletClient.ts +50 -0
  371. package/src/legacy/client.ts +1652 -0
  372. package/src/legacy/prover.ts +135 -0
  373. package/src/poseidon2.ts +246 -0
  374. package/src/promises/Attestor.ts +25 -0
  375. package/src/promises/EvmPublicClient.ts +39 -0
  376. package/src/promises/EvmWalletClient.ts +63 -0
  377. package/src/promises/Payment.ts +86 -0
  378. package/src/promises/Prover.ts +26 -0
  379. package/src/promises/PublicClient.ts +47 -0
  380. package/src/promises/WalletClient.ts +38 -0
  381. package/src/promises/index.ts +7 -0
  382. package/src/rpc.ts +994 -0
  383. package/src/types.ts +281 -0
  384. package/types/package.json +6 -0
  385. package/dist/LICENSE +0 -1
  386. package/dist/chunk-37PNLRA6.js +0 -2418
  387. package/dist/cli.cjs +0 -3031
  388. package/dist/cli.js +0 -675
  389. package/dist/index.cjs +0 -2451
  390. package/dist/index.js +0 -1
  391. package/dist/package.json +0 -18
  392. package/dist/payments.d.ts +0 -835
  393. /package/{dist → src}/tsdoc-metadata.json +0 -0
@@ -0,0 +1,1652 @@
1
+ import { Duration, Effect, pipe, Schedule } from "effect";
2
+ import type { Address, Hex, PublicClient, WalletClient } from "viem";
3
+ import {
4
+ createPublicClient,
5
+ encodeAbiParameters,
6
+ hexToBigInt,
7
+ http,
8
+ keccak256,
9
+ toRlp,
10
+ } from "viem";
11
+ import { AttestationClient } from "../attestation.js";
12
+ import { Z_ASSET_REGISTRY } from "../constants/z-asset-registry.js";
13
+ import * as evm from "../internal/evm.js";
14
+ import { computeNullifier, computeUnspendableAddress } from "../poseidon2.js";
15
+ import {
16
+ computeStorageSlot,
17
+ deterministicShuffleClients,
18
+ fetchLightClients,
19
+ fetchMptProof,
20
+ IBC_STORE_ABI,
21
+ parseProofJson,
22
+ proofJsonToRedeemParams,
23
+ RpcClient,
24
+ } from "../rpc.js";
25
+ import type {
26
+ BalanceInfo,
27
+ DepositResult,
28
+ FullRedeemResult,
29
+ GenerateProofResult,
30
+ RedemptionHistoryEntry,
31
+ TokenInfo,
32
+ UnionPrivatePaymentsConfig,
33
+ UpdateLightClientResult,
34
+ WitnessData,
35
+ } from "../types.js";
36
+ import { ProverClient } from "./prover.js";
37
+
38
+ const ZERO_ADDRESS: Address = "0x0000000000000000000000000000000000000000";
39
+
40
+ const commonRetry = {
41
+ schedule: Schedule.linear(Duration.seconds(2)),
42
+ times: 6,
43
+ } as const satisfies Effect.Retry.Options<any>;
44
+
45
+ /**
46
+ * @public
47
+ */
48
+ export interface ClientOptions {
49
+ proverUrl?: URL | undefined; // optional, default prover.payments.union.build
50
+ rpcUrl: URL;
51
+ chainId: bigint; // fetch from rpc?
52
+ assetAddress: Address; // hardcode into the sdk
53
+ attestationUrl?: string | undefined;
54
+ attestorApiKey?: string | undefined;
55
+ }
56
+
57
+ /**
58
+ * @public
59
+ */
60
+ export interface UpdateLightClientOptions {
61
+ /** The light client ID to update */
62
+ clientId: number;
63
+ /** Block height to update to (bigint or 'latest') */
64
+ height: bigint | "latest";
65
+ /** viem WalletClient with account and chain configured */
66
+ walletClient: WalletClient;
67
+ }
68
+
69
+ /**
70
+ * @public
71
+ */
72
+ export interface DepositOptions {
73
+ /** The 32-byte secret payment key as a hex string */
74
+ paymentKey: Hex;
75
+ /** Array of 0-4 beneficiary addresses*/
76
+ beneficiaries: Address[];
77
+ /** Amount to deposit (in underlying token's smallest unit) */
78
+ amount: bigint;
79
+ /** viem WalletClient with account and chain configured */
80
+ walletClient: WalletClient;
81
+ }
82
+
83
+ /**
84
+ * @public
85
+ */
86
+ export interface GetBalanceOptions {
87
+ /** The deposit address (unspendable address) */
88
+ depositAddress: Address;
89
+ /** The nullifier for this paymentKey */
90
+ nullifier: bigint;
91
+ /** The light client ID to use for querying the source chain state */
92
+ clientId: number;
93
+ }
94
+
95
+ /**
96
+ * @public
97
+ */
98
+ export interface RedeemOptions {
99
+ /** The 32-byte secret payment key as a hex string */
100
+ paymentKey: Hex;
101
+ /** Array of 0-4 beneficiary addresses */
102
+ beneficiaries: Address[];
103
+ /** The beneficiary address to redeem to */
104
+ beneficiary: Address;
105
+ /** Amount to redeem */
106
+ amount: bigint;
107
+ /** Light client IDs to include in the proof (for anonymity set) */
108
+ clientIds: number[];
109
+ /** The specific light client ID to use for the proof */
110
+ selectedClientId: number;
111
+ /** viem WalletClient with account and chain configured */
112
+ walletClient: WalletClient;
113
+ /** Auto unwrap */
114
+ unwrap?: boolean | undefined;
115
+ }
116
+
117
+ /**
118
+ * Construct a {@link UnionPrivatePayments} client.
119
+ *
120
+ * @throws Error if asset is unrecognized.
121
+ * @public
122
+ */
123
+ export const make = (options: ClientOptions): UnionPrivatePayments => {
124
+ const proverUrl =
125
+ options.proverUrl?.toString() ?? "https://prover.payments.union.build";
126
+
127
+ const attestorUrl =
128
+ options.attestationUrl?.toString() ??
129
+ "https://attestor.payments.union.build/functions/v1/attest";
130
+
131
+ const zAssetAddress =
132
+ // @ts-expect-error
133
+ Z_ASSET_REGISTRY[`${options.chainId}`][options.assetAddress];
134
+
135
+ if (!zAssetAddress) throw new Error("Invalid asset address");
136
+ if (!options.attestorApiKey) throw new Error("No attestor API key");
137
+
138
+ return new UnionPrivatePayments({
139
+ proverUrl,
140
+ sourceRpcUrl: options.rpcUrl.toString(),
141
+ destinationRpcUrl: options.rpcUrl.toString(),
142
+ srcZAssetAddress: zAssetAddress,
143
+ dstZAssetAddress: zAssetAddress,
144
+ sourceChainId: options.chainId,
145
+ destinationChainId: options.chainId,
146
+ attestorUrl: attestorUrl,
147
+ attestorApiKey: options.attestorApiKey,
148
+ });
149
+ };
150
+
151
+ /**
152
+ * Union Private Payments client
153
+ *
154
+ * This class provides a high-level interface for performing private transfers
155
+ * using the Union protocol. It handles:
156
+ *
157
+ * - Generating unspendable addresses for deposits
158
+ *
159
+ * - Querying balances (confirmed, pending, redeemed, available)
160
+ *
161
+ * - Building witness data for proof generation
162
+ *
163
+ * - Communicating with the prover server
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * import { UnionPrivatePayments } from '@unionlabs/payments';
168
+ *
169
+ * const client = new UnionPrivatePayments({
170
+ * proverUrl: 'http://localhost:8080',
171
+ * sourceRpcUrl: 'https://eth-mainnet.alchemyapi.io/v2/...',
172
+ * destinationRpcUrl: 'https://arbitrum-mainnet.alchemyapi.io/v2/...',
173
+ * srcZAssetAddress: '0x...', // ZAsset on source chain
174
+ * dstZAssetAddress: '0x...', // ZAsset on destination chain
175
+ * sourceChainId: 1n,
176
+ * destinationChainId: 42161n,
177
+ * });
178
+ *
179
+ * // Generate deposit address
180
+ * const address = client.getDepositAddress(secret, beneficiaries);
181
+ *
182
+ * // Check balance
183
+ * const balance = await client.getBalance({ depositAddress, nullifier, clientId });
184
+ *
185
+ * // Generate proof for redemption
186
+ * const proof = await client.generateProof(secret, beneficiaries, beneficiary, amount, clientIds);
187
+ * ```
188
+ * @public
189
+ */
190
+ export class UnionPrivatePayments {
191
+ private config: UnionPrivatePaymentsConfig;
192
+ private srcClient: RpcClient;
193
+ private dstClient: RpcClient;
194
+ private proverClient: ProverClient;
195
+
196
+ constructor(config: UnionPrivatePaymentsConfig) {
197
+ this.config = config;
198
+ this.srcClient = new RpcClient(config.sourceRpcUrl);
199
+ this.dstClient = new RpcClient(config.destinationRpcUrl);
200
+ this.proverClient = new ProverClient(config.proverUrl);
201
+ }
202
+
203
+ /**
204
+ * Get the deposit address for a given secret and beneficiaries
205
+ *
206
+ * The returned address is an "unspendable" address derived from the secret.
207
+ * Tokens sent to this address can only be redeemed via ZK proof.
208
+ *
209
+ * @param secret - The 32-byte secret as a hex string
210
+ * @param beneficiaries - Array of 1-4 beneficiary addresses (remaining slots zero-padded)
211
+ * @returns The unspendable deposit address
212
+ */
213
+ getDepositAddress(secret: Hex, beneficiaries: Address[]): Address {
214
+ const paddedBeneficiaries = this.padBeneficiaries(beneficiaries);
215
+ return computeUnspendableAddress(
216
+ secret,
217
+ this.config.destinationChainId,
218
+ paddedBeneficiaries,
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Get the nullifier for a given paymentKey
224
+ *
225
+ * The nullifier is used to prevent double-spending. Each (paymentKey, chainId) pair
226
+ * produces a unique nullifier that is recorded on-chain when funds are redeemed.
227
+ *
228
+ * @param paymentKey - The 32-byte paymentKey as a hex string
229
+ * @returns The nullifier as a bigint
230
+ */
231
+ getNullifier(paymentKey: Hex): bigint {
232
+ return computeNullifier(paymentKey, this.config.destinationChainId);
233
+ }
234
+
235
+ /**
236
+ * Get balance information
237
+ *
238
+ * Returns:
239
+ * - confirmed: balance visible to light client (provable)
240
+ * - redeemed: amount already redeemed via nullifier
241
+ * - available: amount that can be redeemed now (confirmed - redeemed)
242
+ * - pending: deposits not yet visible to light client
243
+ *
244
+ * @param options - Options for getting balance
245
+ * @returns Balance information
246
+ */
247
+ async getBalance(options: GetBalanceOptions): Promise<BalanceInfo> {
248
+ const { depositAddress, nullifier, clientId } = options;
249
+ console.log(options);
250
+ console.log({ dstZAssetAddress: this.config.dstZAssetAddress });
251
+ const ibcHandlerAddress = await this.dstClient.getIbcHandlerAddress(
252
+ this.config.dstZAssetAddress,
253
+ );
254
+
255
+ const lightClientAddress = await this.dstClient.getLightClientAddress(
256
+ ibcHandlerAddress,
257
+ clientId,
258
+ );
259
+ const lightClientHeight = await this.dstClient.getLatestHeight(
260
+ lightClientAddress,
261
+ clientId,
262
+ );
263
+
264
+ const balance = await this.getBalanceAtHeight(
265
+ depositAddress,
266
+ nullifier,
267
+ lightClientHeight,
268
+ );
269
+
270
+ return {
271
+ ...balance,
272
+ lightClientHeight,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Get balance information at a specific block height
278
+ *
279
+ * @param depositAddress - The deposit address (unspendable address)
280
+ * @param nullifier - The nullifier for this secret
281
+ * @param height - The block height to query confirmed balance at
282
+ * @returns Balance information at the given height
283
+ */
284
+ async getBalanceAtHeight(
285
+ depositAddress: Address,
286
+ nullifier: bigint,
287
+ height: bigint,
288
+ ): Promise<Omit<BalanceInfo, "lightClientHeight">> {
289
+ const latestHeight = await this.srcClient.getLatestBlockNumber();
290
+
291
+ const [balanceAtTip, confirmed, redeemed] = await Promise.all([
292
+ this.srcClient.getBalance(this.config.srcZAssetAddress, depositAddress),
293
+ this.srcClient.getBalance(
294
+ this.config.srcZAssetAddress,
295
+ depositAddress,
296
+ height,
297
+ ),
298
+ this.dstClient.getNullifierBalance(
299
+ this.config.dstZAssetAddress,
300
+ nullifier,
301
+ ),
302
+ ]);
303
+
304
+ const available = confirmed > redeemed ? confirmed - redeemed : 0n;
305
+ const pending = balanceAtTip > confirmed ? balanceAtTip - confirmed : 0n;
306
+
307
+ return {
308
+ confirmed,
309
+ redeemed,
310
+ available,
311
+ pending,
312
+ latestHeight,
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Generate a proof for redeeming funds
318
+ *
319
+ * This method:
320
+ * 1. Fetches light client data from the destination chain
321
+ * 2. Deterministically shuffles clients for privacy
322
+ * 3. Fetches MPT proof from the source chain
323
+ * 4. Builds the witness data
324
+ * 5. Sends the witness to the prover server
325
+ * 6. Polls until the proof is ready
326
+ *
327
+ * @param secret - The 32-byte secret as a hex string
328
+ * @param beneficiaries - Array of 0-4 beneficiary addresses (empty array = unbounded mode)
329
+ * @param beneficiary - The beneficiary address to redeem to
330
+ * @param amount - Amount to redeem
331
+ * @param clientIds - Light client IDs to include in the proof (for anonymity set)
332
+ * @param selectedClientId - The specific light client ID to use for the proof
333
+ * @returns GenerateProofResult containing proof result and client-side metadata
334
+ */
335
+ async generateProof(
336
+ secret: Hex,
337
+ beneficiaries: Address[],
338
+ beneficiary: Address,
339
+ amount: bigint,
340
+ clientIds: number[],
341
+ selectedClientId: number,
342
+ ): Promise<GenerateProofResult> {
343
+ // Validate inputs
344
+ if (!clientIds.includes(selectedClientId)) {
345
+ return {
346
+ proof: {
347
+ success: false,
348
+ error: `selectedClientId ${selectedClientId} not in clientIds`,
349
+ },
350
+ };
351
+ }
352
+ if (beneficiary === ZERO_ADDRESS) {
353
+ return {
354
+ proof: {
355
+ success: false,
356
+ error: "Beneficiary address cannot be zero",
357
+ },
358
+ };
359
+ }
360
+
361
+ const paddedBeneficiaries = this.padBeneficiaries(beneficiaries);
362
+ const depositAddress = this.getDepositAddress(secret, beneficiaries);
363
+
364
+ const lightClients = await fetchLightClients(
365
+ this.dstClient,
366
+ this.config.dstZAssetAddress,
367
+ clientIds,
368
+ );
369
+
370
+ if (lightClients.length === 0) {
371
+ return {
372
+ proof: {
373
+ success: false,
374
+ error: "No valid light clients found",
375
+ },
376
+ };
377
+ }
378
+
379
+ // Deterministic shuffle: same secret always produces the same ordering for privacy
380
+ const shuffled = deterministicShuffleClients(lightClients, secret);
381
+ const selectedClientIndex = shuffled.findIndex(
382
+ (c) => c.clientId === selectedClientId,
383
+ );
384
+ if (selectedClientIndex === -1) {
385
+ return {
386
+ proof: {
387
+ success: false,
388
+ error: `Client ${selectedClientId} not found after fetching`,
389
+ },
390
+ };
391
+ }
392
+ const selectedClient = shuffled[selectedClientIndex]!;
393
+
394
+ const { tokenAddressKey, balanceSlot } =
395
+ await this.dstClient.getCounterparty(
396
+ this.config.dstZAssetAddress,
397
+ selectedClientId,
398
+ );
399
+
400
+ // Check if counterparty is configured for this light client
401
+ const ZERO_BYTES32 =
402
+ "0x0000000000000000000000000000000000000000000000000000000000000000";
403
+ if (balanceSlot === ZERO_BYTES32 || tokenAddressKey === ZERO_BYTES32) {
404
+ return {
405
+ proof: {
406
+ success: false,
407
+ error:
408
+ `Light client ${selectedClientId} is not configured as a counterparty on the destination ZAsset. ` +
409
+ `Please call setCounterparty() on the ZAsset contract first.`,
410
+ },
411
+ };
412
+ }
413
+
414
+ const mappingSlot = hexToBigInt(balanceSlot);
415
+
416
+ const nullifier = this.getNullifier(secret);
417
+
418
+ // Check balance at the selected light client's height
419
+ const balance = await this.getBalanceAtHeight(
420
+ depositAddress,
421
+ nullifier,
422
+ selectedClient.height,
423
+ );
424
+
425
+ if (amount > balance.available) {
426
+ return {
427
+ proof: {
428
+ success: false,
429
+ error:
430
+ `Insufficient available balance. Requested: ${amount}, Available: ${balance.available} ` +
431
+ `(Confirmed at height ${selectedClient.height}: ${balance.confirmed}, Already redeemed: ${balance.redeemed})`,
432
+ },
433
+ };
434
+ }
435
+
436
+ const storageSlot = computeStorageSlot(depositAddress, mappingSlot);
437
+
438
+ const mptProof = await fetchMptProof(
439
+ this.srcClient,
440
+ this.config.srcZAssetAddress,
441
+ storageSlot,
442
+ selectedClient.height,
443
+ );
444
+
445
+ const witness: WitnessData = {
446
+ secret,
447
+ dstChainId: this.config.destinationChainId,
448
+ beneficiaries: paddedBeneficiaries,
449
+ beneficiary,
450
+ redeemAmount: amount,
451
+ alreadyRedeemed: balance.redeemed,
452
+ lightClients: shuffled,
453
+ selectedClientIndex,
454
+ mptProof,
455
+ srcZAssetAddress: this.config.srcZAssetAddress,
456
+ mappingSlot: `0x${mappingSlot.toString(16)}` as Hex,
457
+ };
458
+
459
+ const proofResult = await this.proverClient.generateProof(witness);
460
+
461
+ if (proofResult.success) {
462
+ return {
463
+ proof: proofResult,
464
+ metadata: {
465
+ depositAddress,
466
+ beneficiary,
467
+ value: amount,
468
+ lightClients: shuffled,
469
+ nullifier,
470
+ },
471
+ };
472
+ }
473
+
474
+ return { proof: proofResult };
475
+ }
476
+
477
+ /**
478
+ * Export the verifier contract from the prover server
479
+ *
480
+ * The verifier contract is used to verify proofs on-chain.
481
+ * It is circuit-specific and does not change between proofs.
482
+ *
483
+ * @returns The Solidity verifier contract source code
484
+ */
485
+ async exportVerifier(): Promise<string> {
486
+ return this.proverClient.exportVerifier();
487
+ }
488
+
489
+ /**
490
+ * Get source ZAsset token information
491
+ *
492
+ * @returns Token symbol and decimals
493
+ */
494
+ async getSrcZAssetInfo(): Promise<TokenInfo> {
495
+ const [symbol, decimals] = await Promise.all([
496
+ this.srcClient.getSymbol(this.config.srcZAssetAddress),
497
+ this.srcClient.getDecimals(this.config.srcZAssetAddress),
498
+ ]);
499
+ return { symbol, decimals };
500
+ }
501
+
502
+ /**
503
+ * Get destination ZAsset token information
504
+ *
505
+ * @returns Token symbol and decimals
506
+ */
507
+ async getDstZAssetInfo(): Promise<TokenInfo> {
508
+ const [symbol, decimals] = await Promise.all([
509
+ this.dstClient.getSymbol(this.config.dstZAssetAddress),
510
+ this.dstClient.getDecimals(this.config.dstZAssetAddress),
511
+ ]);
512
+ return { symbol, decimals };
513
+ }
514
+
515
+ /**
516
+ * Deposit underlying tokens to ZAsset and transfer to deposit address
517
+ *
518
+ * Executes 3 transactions: approve → deposit → transfer.
519
+ * The wallet client must be connected to the source chain.
520
+ *
521
+ * @param secret - The 32-byte secret as a hex string
522
+ * @param beneficiaries - Array of 0-4 beneficiary addresses
523
+ * @param amount - Amount to deposit (in underlying token's smallest unit)
524
+ * @param walletClient - viem WalletClient with account and chain configured
525
+ * @returns Transaction hash, deposit address, underlying token, and chain ID
526
+ */
527
+ async deposit(options: DepositOptions): Promise<DepositResult> {
528
+ const { paymentKey, beneficiaries, amount, walletClient } = options;
529
+
530
+ if (!walletClient.account) {
531
+ throw new Error("WalletClient must have an account");
532
+ }
533
+ if (!walletClient.chain) {
534
+ throw new Error("WalletClient must have a chain configured");
535
+ }
536
+
537
+ return Effect.gen(this, function* () {
538
+ const depositAddress = this.getDepositAddress(paymentKey, beneficiaries);
539
+
540
+ // Get underlying token address
541
+ const underlyingToken = yield* pipe(
542
+ evm.readContract({
543
+ address: this.config.srcZAssetAddress,
544
+ abi: [
545
+ {
546
+ inputs: [],
547
+ name: "underlying",
548
+ outputs: [{ name: "", type: "address" }],
549
+ stateMutability: "view",
550
+ type: "function",
551
+ },
552
+ ] as const,
553
+ functionName: "underlying",
554
+ }),
555
+ Effect.retry(commonRetry),
556
+ Effect.provideService(evm.PublicClient, {
557
+ client: this.srcClient.getClient(),
558
+ }),
559
+ );
560
+
561
+ if (underlyingToken === ZERO_ADDRESS) {
562
+ return yield* Effect.fail(
563
+ Error("ZAsset is not a wrapped token (underlying is zero address)"),
564
+ );
565
+ }
566
+
567
+ const ERC20_ABI = [
568
+ {
569
+ inputs: [
570
+ { name: "spender", type: "address" },
571
+ { name: "amount", type: "uint256" },
572
+ ],
573
+ name: "approve",
574
+ outputs: [{ name: "", type: "bool" }],
575
+ stateMutability: "nonpayable",
576
+ type: "function",
577
+ },
578
+ ] as const;
579
+
580
+ const ZASSET_ABI = [
581
+ {
582
+ inputs: [{ name: "amount", type: "uint256" }],
583
+ name: "deposit",
584
+ outputs: [],
585
+ stateMutability: "nonpayable",
586
+ type: "function",
587
+ },
588
+ {
589
+ inputs: [
590
+ { name: "to", type: "address" },
591
+ { name: "amount", type: "uint256" },
592
+ ],
593
+ name: "transfer",
594
+ outputs: [{ name: "", type: "bool" }],
595
+ stateMutability: "nonpayable",
596
+ type: "function",
597
+ },
598
+ ] as const;
599
+
600
+ // 1. Approve ZAsset to spend underlying tokens
601
+ const approveHash = yield* pipe(
602
+ evm.writeContract({
603
+ address: underlyingToken,
604
+ abi: ERC20_ABI,
605
+ functionName: "approve",
606
+ args: [this.config.srcZAssetAddress, amount],
607
+ chain: walletClient.chain,
608
+ account: walletClient.account!,
609
+ }),
610
+ Effect.retry(commonRetry),
611
+ );
612
+ const approveReceipt = yield* pipe(
613
+ evm.waitForTransactionReceipt(approveHash),
614
+ Effect.retry(commonRetry),
615
+ );
616
+ if (approveReceipt.status === "reverted") {
617
+ return yield* Effect.fail(
618
+ Error(`Approve transaction reverted: ${approveHash}`),
619
+ );
620
+ }
621
+
622
+ // 2. Deposit underlying to get ZAsset
623
+ const depositHash = yield* pipe(
624
+ evm.writeContract({
625
+ address: this.config.srcZAssetAddress,
626
+ abi: ZASSET_ABI,
627
+ functionName: "deposit",
628
+ args: [amount],
629
+ chain: walletClient.chain,
630
+ account: walletClient.account!,
631
+ }),
632
+ Effect.retry(commonRetry),
633
+ );
634
+ const depositReceipt = yield* evm
635
+ .waitForTransactionReceipt(depositHash)
636
+ .pipe(Effect.retry(commonRetry));
637
+ if (depositReceipt.status === "reverted") {
638
+ throw new Error(`Deposit transaction reverted: ${depositHash}`);
639
+ }
640
+
641
+ // 3. Transfer ZAsset to deposit address
642
+ const transferHash = yield* pipe(
643
+ evm.writeContract({
644
+ address: this.config.srcZAssetAddress,
645
+ abi: ZASSET_ABI,
646
+ functionName: "transfer",
647
+ args: [depositAddress, amount],
648
+ chain: walletClient.chain,
649
+ account: walletClient.account!,
650
+ }),
651
+ Effect.retry(commonRetry),
652
+ );
653
+ const transferReceipt =
654
+ yield* evm.waitForTransactionReceipt(transferHash);
655
+ if (transferReceipt.status === "reverted") {
656
+ throw new Error(`Transfer transaction reverted: ${transferHash}`);
657
+ }
658
+
659
+ return {
660
+ txHash: transferHash,
661
+ depositAddress,
662
+ underlyingToken,
663
+ chainId: this.config.sourceChainId,
664
+ height: transferReceipt.blockNumber,
665
+ };
666
+ }).pipe(
667
+ Effect.provide(
668
+ evm.PublicClient.Live({
669
+ chain: options.walletClient.chain,
670
+ transport: http(this.config.sourceRpcUrl),
671
+ }),
672
+ ),
673
+ Effect.provideService(evm.WalletClient, {
674
+ client: options.walletClient,
675
+ account: options.walletClient.account!,
676
+ chain: options.walletClient.chain!,
677
+ }),
678
+ Effect.runPromise,
679
+ );
680
+ }
681
+
682
+ /**
683
+ * Deposit underlying tokens to ZAsset and transfer to deposit address
684
+ *
685
+ * Executes 3 transactions: approve → deposit → transfer.
686
+ * The wallet client must be connected to the source chain.
687
+ *
688
+ * @param secret - The 32-byte secret as a hex string
689
+ * @param beneficiaries - Array of 0-4 beneficiary addresses
690
+ * @param amount - Amount to deposit (in underlying token's smallest unit)
691
+ * @param walletClient - viem WalletClient with account and chain configured
692
+ * @returns Transaction hash, deposit address, underlying token, and chain ID
693
+ */
694
+ async unsafeDeposit(options: DepositOptions): Promise<DepositResult> {
695
+ const { paymentKey, beneficiaries, amount, walletClient } = options;
696
+ if (!walletClient.account) {
697
+ throw new Error("WalletClient must have an account");
698
+ }
699
+ if (!walletClient.chain) {
700
+ throw new Error("WalletClient must have a chain configured");
701
+ }
702
+
703
+ const depositAddress = this.getDepositAddress(paymentKey, beneficiaries);
704
+ const publicClient = createPublicClient({
705
+ chain: walletClient.chain,
706
+ transport: http(this.config.sourceRpcUrl),
707
+ });
708
+
709
+ // Get underlying token address
710
+ const underlyingToken = (await this.srcClient.getClient().readContract({
711
+ address: this.config.srcZAssetAddress,
712
+ abi: [
713
+ {
714
+ inputs: [],
715
+ name: "underlying",
716
+ outputs: [{ name: "", type: "address" }],
717
+ stateMutability: "view",
718
+ type: "function",
719
+ },
720
+ ] as const,
721
+ functionName: "underlying",
722
+ })) as Address;
723
+
724
+ if (underlyingToken === ZERO_ADDRESS) {
725
+ throw new Error(
726
+ "ZAsset is not a wrapped token (underlying is zero address)",
727
+ );
728
+ }
729
+
730
+ const ERC20_ABI = [
731
+ {
732
+ inputs: [
733
+ { name: "spender", type: "address" },
734
+ { name: "amount", type: "uint256" },
735
+ ],
736
+ name: "approve",
737
+ outputs: [{ name: "", type: "bool" }],
738
+ stateMutability: "nonpayable",
739
+ type: "function",
740
+ },
741
+ ] as const;
742
+
743
+ const ZASSET_ABI = [
744
+ {
745
+ inputs: [{ name: "amount", type: "uint256" }],
746
+ name: "deposit",
747
+ outputs: [],
748
+ stateMutability: "nonpayable",
749
+ type: "function",
750
+ },
751
+ {
752
+ inputs: [
753
+ { name: "to", type: "address" },
754
+ { name: "amount", type: "uint256" },
755
+ ],
756
+ name: "transfer",
757
+ outputs: [{ name: "", type: "bool" }],
758
+ stateMutability: "nonpayable",
759
+ type: "function",
760
+ },
761
+ ] as const;
762
+
763
+ // 1. Approve ZAsset to spend underlying tokens
764
+ const approveHash = await walletClient
765
+ .writeContractSync({
766
+ address: underlyingToken,
767
+ abi: ERC20_ABI,
768
+ functionName: "approve",
769
+ args: [this.config.srcZAssetAddress, amount],
770
+ chain: walletClient.chain,
771
+ account: walletClient.account,
772
+ })
773
+ .then((x) => x.transactionHash);
774
+ const approveReceipt = await publicClient.waitForTransactionReceipt({
775
+ hash: approveHash,
776
+ });
777
+ if (approveReceipt.status === "reverted") {
778
+ throw new Error(`Approve transaction reverted: ${approveHash}`);
779
+ }
780
+
781
+ // 2. Deposit underlying to get ZAsset
782
+ const depositHash = await walletClient
783
+ .writeContractSync({
784
+ address: this.config.srcZAssetAddress,
785
+ abi: ZASSET_ABI,
786
+ functionName: "deposit",
787
+ args: [amount],
788
+ chain: walletClient.chain,
789
+ account: walletClient.account,
790
+ })
791
+ .then((x) => x.transactionHash);
792
+ const depositReceipt = await publicClient.waitForTransactionReceipt({
793
+ hash: depositHash,
794
+ });
795
+ if (depositReceipt.status === "reverted") {
796
+ throw new Error(`Deposit transaction reverted: ${depositHash}`);
797
+ }
798
+
799
+ // 3. Transfer ZAsset to deposit address
800
+ const transferHash = await walletClient
801
+ .writeContractSync({
802
+ address: this.config.srcZAssetAddress,
803
+ abi: ZASSET_ABI,
804
+ functionName: "transfer",
805
+ args: [depositAddress, amount],
806
+ chain: walletClient.chain,
807
+ account: walletClient.account,
808
+ })
809
+ .then((x) => x.transactionHash);
810
+ const transferReceipt = await publicClient.waitForTransactionReceipt({
811
+ hash: transferHash,
812
+ });
813
+ if (transferReceipt.status === "reverted") {
814
+ throw new Error(`Transfer transaction reverted: ${transferHash}`);
815
+ }
816
+
817
+ return {
818
+ txHash: transferHash,
819
+ depositAddress,
820
+ underlyingToken,
821
+ chainId: this.config.sourceChainId,
822
+ height: transferReceipt.blockNumber,
823
+ };
824
+ }
825
+
826
+ /**
827
+ * Update loopback light client to a specific block height
828
+ *
829
+ * Fetches the IBC handler address internally from the destination ZAsset.
830
+ * The wallet client must be connected to the destination chain.
831
+ *
832
+ * @returns Transaction hash, block number, state root, and chain ID
833
+ */
834
+ async updateLightClient(
835
+ options: UpdateLightClientOptions,
836
+ ): Promise<UpdateLightClientResult> {
837
+ const { clientId, height, walletClient } = options;
838
+
839
+ if (!walletClient.account) {
840
+ throw new Error("WalletClient must have an account");
841
+ }
842
+ if (!walletClient.chain) {
843
+ throw new Error("WalletClient must have a chain configured");
844
+ }
845
+
846
+ return Effect.gen(this, function* () {
847
+ const publicClient = createPublicClient({
848
+ chain: walletClient.chain,
849
+ transport: http(this.config.destinationRpcUrl),
850
+ });
851
+
852
+ // Get IBC handler address from ZAsset
853
+ const ibcHandlerAddress = yield* Effect.tryPromise({
854
+ try: () =>
855
+ this.dstClient.getIbcHandlerAddress(this.config.dstZAssetAddress),
856
+ catch: (cause) => Effect.fail(cause as Error),
857
+ });
858
+
859
+ // Get the block number
860
+ const blockNumber =
861
+ height === "latest"
862
+ ? yield* pipe(
863
+ Effect.tryPromise(() => publicClient.getBlockNumber()),
864
+ Effect.retry(commonRetry),
865
+ )
866
+ : height;
867
+
868
+ // Get the block for metadata
869
+ const block = yield* pipe(
870
+ Effect.tryPromise(() => publicClient.getBlock({ blockNumber })),
871
+ Effect.retry(commonRetry),
872
+ );
873
+
874
+ if (!block.number) {
875
+ throw new Error("Block number is null");
876
+ }
877
+
878
+ // Helper to convert bigint/number to minimal RLP hex encoding
879
+ const toRlpHex = (value: bigint | number | null | undefined): Hex => {
880
+ if (
881
+ value === undefined ||
882
+ value === null ||
883
+ value === 0n ||
884
+ value === 0
885
+ ) {
886
+ return "0x" as Hex;
887
+ }
888
+ let hex =
889
+ typeof value === "bigint" ? value.toString(16) : value.toString(16);
890
+ if (hex.length % 2 !== 0) {
891
+ hex = "0" + hex;
892
+ }
893
+ return `0x${hex}` as Hex;
894
+ };
895
+
896
+ // Build header fields in order (pre-merge + post-merge fields)
897
+ const headerFields: (Hex | Hex[])[] = [
898
+ block.parentHash,
899
+ block.sha3Uncles,
900
+ block.miner,
901
+ block.stateRoot,
902
+ block.transactionsRoot,
903
+ block.receiptsRoot,
904
+ block.logsBloom ?? (("0x" + "00".repeat(256)) as Hex),
905
+ toRlpHex(block.difficulty),
906
+ toRlpHex(block.number),
907
+ toRlpHex(block.gasLimit),
908
+ toRlpHex(block.gasUsed),
909
+ toRlpHex(block.timestamp),
910
+ block.extraData,
911
+ block.mixHash ??
912
+ ("0x0000000000000000000000000000000000000000000000000000000000000000" as Hex),
913
+ block.nonce ?? ("0x0000000000000000" as Hex),
914
+ ];
915
+
916
+ // Post-merge fields
917
+ if (block.baseFeePerGas !== undefined && block.baseFeePerGas !== null) {
918
+ headerFields.push(toRlpHex(block.baseFeePerGas));
919
+ }
920
+ if (block.withdrawalsRoot) {
921
+ headerFields.push(block.withdrawalsRoot);
922
+ }
923
+ if (block.blobGasUsed !== undefined && block.blobGasUsed !== null) {
924
+ headerFields.push(toRlpHex(block.blobGasUsed));
925
+ }
926
+ if (block.excessBlobGas !== undefined && block.excessBlobGas !== null) {
927
+ headerFields.push(toRlpHex(block.excessBlobGas));
928
+ }
929
+ if (block.parentBeaconBlockRoot) {
930
+ headerFields.push(block.parentBeaconBlockRoot);
931
+ }
932
+ const blockAny = block as Record<string, unknown>;
933
+ if (blockAny.requestsHash) {
934
+ headerFields.push(blockAny.requestsHash as Hex);
935
+ }
936
+
937
+ const rlpEncoded = toRlp(headerFields);
938
+
939
+ // Verify the encoding produces the correct block hash
940
+ const computedHash = keccak256(rlpEncoded);
941
+ if (computedHash !== block.hash) {
942
+ throw new Error(
943
+ `RLP encoding mismatch: computed hash ${computedHash} does not match block hash ${block.hash}`,
944
+ );
945
+ }
946
+
947
+ // Encode the Header struct: (uint64 height, bytes encodedHeader)
948
+ const clientMessage = encodeAbiParameters(
949
+ [
950
+ { type: "uint64", name: "height" },
951
+ { type: "bytes", name: "encodedHeader" },
952
+ ],
953
+ [block.number, rlpEncoded],
954
+ );
955
+
956
+ const LIGHTCLIENT_ABI = [
957
+ {
958
+ inputs: [
959
+ { name: "caller", type: "address" },
960
+ { name: "clientId", type: "uint32" },
961
+ { name: "clientMessage", type: "bytes" },
962
+ { name: "relayer", type: "address" },
963
+ ],
964
+ name: "updateClient",
965
+ outputs: [],
966
+ stateMutability: "nonpayable",
967
+ type: "function",
968
+ },
969
+ ] as const;
970
+
971
+ // Wait for the next block so the fetched block's hash is verifiable on-chain
972
+ let currentBlock = yield* pipe(
973
+ Effect.tryPromise(() => publicClient.getBlockNumber()),
974
+ Effect.retry(commonRetry),
975
+ );
976
+ while (currentBlock <= blockNumber) {
977
+ yield* Effect.sleep("1 second");
978
+ currentBlock = yield* Effect.tryPromise(() =>
979
+ publicClient.getBlockNumber(),
980
+ );
981
+ }
982
+
983
+ const loopbackClient = yield* pipe(
984
+ evm.readContract({
985
+ address: ibcHandlerAddress,
986
+ abi: IBC_STORE_ABI,
987
+ functionName: "getClient",
988
+ args: [clientId],
989
+ }),
990
+ Effect.retry(commonRetry),
991
+ );
992
+
993
+ // Submit the transaction
994
+ const txHash = yield* pipe(
995
+ evm.writeContract({
996
+ address: loopbackClient,
997
+ abi: LIGHTCLIENT_ABI,
998
+ functionName: "updateClient",
999
+ args: [
1000
+ walletClient.account!.address,
1001
+ clientId,
1002
+ clientMessage,
1003
+ walletClient.account!.address,
1004
+ ],
1005
+ chain: walletClient.chain,
1006
+ account: walletClient.account!,
1007
+ }),
1008
+ Effect.retry(commonRetry),
1009
+ );
1010
+
1011
+ const chainId = yield* Effect.sync(() => this.config.destinationChainId);
1012
+
1013
+ return {
1014
+ txHash,
1015
+ blockNumber: block.number,
1016
+ stateRoot: block.stateRoot,
1017
+ chainId,
1018
+ };
1019
+ }).pipe(
1020
+ Effect.provide(
1021
+ evm.PublicClient.Live({
1022
+ chain: walletClient.chain,
1023
+ transport: http(this.config.destinationRpcUrl),
1024
+ }),
1025
+ ),
1026
+ Effect.provideService(evm.WalletClient, {
1027
+ client: options.walletClient,
1028
+ account: options.walletClient.account!,
1029
+ chain: options.walletClient.chain!,
1030
+ }),
1031
+ Effect.runPromise,
1032
+ );
1033
+ }
1034
+
1035
+ /**
1036
+ * Update loopback light client to a specific block height
1037
+ *
1038
+ * Fetches the IBC handler address internally from the destination ZAsset.
1039
+ * The wallet client must be connected to the destination chain.
1040
+ *
1041
+ * @returns Transaction hash, block number, state root, and chain ID
1042
+ */
1043
+ async unsafeUpdateLightClient(
1044
+ options: UpdateLightClientOptions,
1045
+ ): Promise<UpdateLightClientResult> {
1046
+ const { clientId, height, walletClient } = options;
1047
+
1048
+ if (!walletClient.account) {
1049
+ throw new Error("WalletClient must have an account");
1050
+ }
1051
+ if (!walletClient.chain) {
1052
+ throw new Error("WalletClient must have a chain configured");
1053
+ }
1054
+
1055
+ const publicClient = createPublicClient({
1056
+ chain: walletClient.chain,
1057
+ transport: http(this.config.destinationRpcUrl),
1058
+ });
1059
+
1060
+ // Get IBC handler address from ZAsset
1061
+ const ibcHandlerAddress = await this.dstClient.getIbcHandlerAddress(
1062
+ this.config.dstZAssetAddress,
1063
+ );
1064
+
1065
+ // Get the block number
1066
+ const blockNumber =
1067
+ height === "latest" ? await publicClient.getBlockNumber() : height;
1068
+
1069
+ // Get the block for metadata
1070
+ const block = await publicClient.getBlock({ blockNumber });
1071
+
1072
+ if (!block.number) {
1073
+ throw new Error("Block number is null");
1074
+ }
1075
+
1076
+ // Helper to convert bigint/number to minimal RLP hex encoding
1077
+ const toRlpHex = (value: bigint | number | null | undefined): Hex => {
1078
+ if (
1079
+ value === undefined ||
1080
+ value === null ||
1081
+ value === 0n ||
1082
+ value === 0
1083
+ ) {
1084
+ return "0x" as Hex;
1085
+ }
1086
+ let hex =
1087
+ typeof value === "bigint" ? value.toString(16) : value.toString(16);
1088
+ if (hex.length % 2 !== 0) {
1089
+ hex = "0" + hex;
1090
+ }
1091
+ return `0x${hex}` as Hex;
1092
+ };
1093
+
1094
+ // Build header fields in order (pre-merge + post-merge fields)
1095
+ const headerFields: (Hex | Hex[])[] = [
1096
+ block.parentHash,
1097
+ block.sha3Uncles,
1098
+ block.miner,
1099
+ block.stateRoot,
1100
+ block.transactionsRoot,
1101
+ block.receiptsRoot,
1102
+ block.logsBloom ?? (("0x" + "00".repeat(256)) as Hex),
1103
+ toRlpHex(block.difficulty),
1104
+ toRlpHex(block.number),
1105
+ toRlpHex(block.gasLimit),
1106
+ toRlpHex(block.gasUsed),
1107
+ toRlpHex(block.timestamp),
1108
+ block.extraData,
1109
+ block.mixHash ??
1110
+ ("0x0000000000000000000000000000000000000000000000000000000000000000" as Hex),
1111
+ block.nonce ?? ("0x0000000000000000" as Hex),
1112
+ ];
1113
+
1114
+ // Post-merge fields
1115
+ if (block.baseFeePerGas !== undefined && block.baseFeePerGas !== null) {
1116
+ headerFields.push(toRlpHex(block.baseFeePerGas));
1117
+ }
1118
+ if (block.withdrawalsRoot) {
1119
+ headerFields.push(block.withdrawalsRoot);
1120
+ }
1121
+ if (block.blobGasUsed !== undefined && block.blobGasUsed !== null) {
1122
+ headerFields.push(toRlpHex(block.blobGasUsed));
1123
+ }
1124
+ if (block.excessBlobGas !== undefined && block.excessBlobGas !== null) {
1125
+ headerFields.push(toRlpHex(block.excessBlobGas));
1126
+ }
1127
+ if (block.parentBeaconBlockRoot) {
1128
+ headerFields.push(block.parentBeaconBlockRoot);
1129
+ }
1130
+ const blockAny = block as Record<string, unknown>;
1131
+ if (blockAny.requestsHash) {
1132
+ headerFields.push(blockAny.requestsHash as Hex);
1133
+ }
1134
+
1135
+ const rlpEncoded = toRlp(headerFields);
1136
+
1137
+ // Verify the encoding produces the correct block hash
1138
+ const computedHash = keccak256(rlpEncoded);
1139
+ if (computedHash !== block.hash) {
1140
+ throw new Error(
1141
+ `RLP encoding mismatch: computed hash ${computedHash} does not match block hash ${block.hash}`,
1142
+ );
1143
+ }
1144
+
1145
+ // Encode the Header struct: (uint64 height, bytes encodedHeader)
1146
+ const clientMessage = encodeAbiParameters(
1147
+ [
1148
+ { type: "uint64", name: "height" },
1149
+ { type: "bytes", name: "encodedHeader" },
1150
+ ],
1151
+ [block.number, rlpEncoded],
1152
+ );
1153
+
1154
+ const LIGHTCLIENT_ABI = [
1155
+ {
1156
+ inputs: [
1157
+ { name: "caller", type: "address" },
1158
+ { name: "clientId", type: "uint32" },
1159
+ { name: "clientMessage", type: "bytes" },
1160
+ { name: "relayer", type: "address" },
1161
+ ],
1162
+ name: "updateClient",
1163
+ outputs: [],
1164
+ stateMutability: "nonpayable",
1165
+ type: "function",
1166
+ },
1167
+ ] as const;
1168
+
1169
+ // Wait for the next block so the fetched block's hash is verifiable on-chain
1170
+ let currentBlock = await publicClient.getBlockNumber();
1171
+ while (currentBlock <= blockNumber) {
1172
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1173
+ currentBlock = await publicClient.getBlockNumber();
1174
+ }
1175
+
1176
+ const loopbackClient = await publicClient.readContract({
1177
+ address: ibcHandlerAddress,
1178
+ abi: IBC_STORE_ABI,
1179
+ functionName: "getClient",
1180
+ args: [clientId],
1181
+ });
1182
+
1183
+ // Submit the transaction
1184
+ const txHash = await walletClient
1185
+ .writeContractSync({
1186
+ address: loopbackClient,
1187
+ abi: LIGHTCLIENT_ABI,
1188
+ functionName: "updateClient",
1189
+ args: [
1190
+ walletClient.account.address,
1191
+ clientId,
1192
+ clientMessage,
1193
+ walletClient.account.address,
1194
+ ],
1195
+ chain: walletClient.chain,
1196
+ account: walletClient.account,
1197
+ })
1198
+ .then((x) => x.transactionHash);
1199
+
1200
+ return {
1201
+ txHash,
1202
+ blockNumber: block.number,
1203
+ stateRoot: block.stateRoot,
1204
+ chainId: this.config.destinationChainId,
1205
+ };
1206
+ }
1207
+
1208
+ /**
1209
+ * Get redemption history for a secret
1210
+ *
1211
+ * Queries the Redeemed events filtered by the nullifier derived from the secret.
1212
+ * This is a read-only operation that doesn't require a wallet.
1213
+ *
1214
+ * @param secret - The 32-byte secret as a hex string
1215
+ * @param fromBlock - Start block number (default: earliest)
1216
+ * @param toBlock - End block number (default: latest)
1217
+ * @returns Array of redemption transactions
1218
+ */
1219
+ getRedemptionHistory(
1220
+ secret: Hex,
1221
+ fromBlock?: bigint,
1222
+ toBlock?: bigint,
1223
+ ): Promise<RedemptionHistoryEntry[]> {
1224
+ return Effect.gen(this, function* () {
1225
+ const nullifier = this.getNullifier(secret);
1226
+ return yield* pipe(
1227
+ Effect.tryPromise(() =>
1228
+ this.dstClient.getRedemptionHistory(
1229
+ this.config.dstZAssetAddress,
1230
+ nullifier,
1231
+ fromBlock,
1232
+ toBlock,
1233
+ ),
1234
+ ),
1235
+ Effect.retry(commonRetry),
1236
+ );
1237
+ }).pipe(Effect.runPromise);
1238
+ }
1239
+
1240
+ /**
1241
+ * Get redemption history for a secret
1242
+ *
1243
+ * Queries the Redeemed events filtered by the nullifier derived from the secret.
1244
+ * This is a read-only operation that doesn't require a wallet.
1245
+ *
1246
+ * @param secret - The 32-byte secret as a hex string
1247
+ * @param fromBlock - Start block number (default: earliest)
1248
+ * @param toBlock - End block number (default: latest)
1249
+ * @returns Array of redemption transactions
1250
+ */
1251
+ async unsafeGetRedemptionHistory(
1252
+ secret: Hex,
1253
+ fromBlock?: bigint,
1254
+ toBlock?: bigint,
1255
+ ): Promise<RedemptionHistoryEntry[]> {
1256
+ const nullifier = this.getNullifier(secret);
1257
+ return this.dstClient.getRedemptionHistory(
1258
+ this.config.dstZAssetAddress,
1259
+ nullifier,
1260
+ fromBlock,
1261
+ toBlock,
1262
+ );
1263
+ }
1264
+
1265
+ /**
1266
+ * Full redeem flow: generate proof + get attestation + submit transaction
1267
+ *
1268
+ * This method orchestrates the entire redemption process:
1269
+ * 1. Generates a ZK proof via the prover server
1270
+ * 2. Gets attestation from the attestation service
1271
+ * 3. Submits the redeem transaction
1272
+ *
1273
+ * The wallet client must be connected to the destination chain.
1274
+ *
1275
+ * @returns Transaction hash, proof, and metadata
1276
+ */
1277
+ redeem(options: RedeemOptions): Promise<FullRedeemResult> {
1278
+ const {
1279
+ paymentKey,
1280
+ beneficiaries,
1281
+ beneficiary,
1282
+ amount,
1283
+ clientIds,
1284
+ selectedClientId,
1285
+ walletClient,
1286
+ unwrap = true,
1287
+ } = options;
1288
+
1289
+ // Resolve attestation URL and API key from options or stored config
1290
+ const attestationUrl = this.config.attestorUrl;
1291
+ const attestorApiKey = this.config.attestorApiKey;
1292
+
1293
+ if (!attestationUrl) {
1294
+ throw Error(
1295
+ "Attestation URL must be provided either in redeem options or ClientOptions",
1296
+ );
1297
+ }
1298
+ if (!attestorApiKey) {
1299
+ throw Error(
1300
+ "Attestation API key must be provided either in redeem options or ClientOptions",
1301
+ );
1302
+ }
1303
+
1304
+ if (!walletClient.account) {
1305
+ throw Error("WalletClient must have an account");
1306
+ }
1307
+ if (!walletClient.chain) {
1308
+ throw Error("WalletClient must have a chain configured");
1309
+ }
1310
+
1311
+ return Effect.gen(this, function* () {
1312
+ // 1. Generate proof
1313
+ const proofResult = yield* Effect.tryPromise({
1314
+ try: () =>
1315
+ this.generateProof(
1316
+ paymentKey,
1317
+ beneficiaries,
1318
+ beneficiary,
1319
+ amount,
1320
+ clientIds,
1321
+ selectedClientId,
1322
+ ),
1323
+ catch: (cause) => Effect.fail(cause as Error),
1324
+ }).pipe(Effect.retry(commonRetry));
1325
+
1326
+ if (
1327
+ !proofResult.proof.success ||
1328
+ !proofResult.proof.proofJson ||
1329
+ !proofResult.metadata
1330
+ ) {
1331
+ return yield* Effect.fail(
1332
+ Error(proofResult.proof.error ?? "Proof generation failed"),
1333
+ );
1334
+ }
1335
+
1336
+ const proofJson = parseProofJson(proofResult.proof.proofJson);
1337
+ const metadata = proofResult.metadata;
1338
+ const depositAddress = this.getDepositAddress(paymentKey, beneficiaries);
1339
+
1340
+ // 2. Get attestation
1341
+ const attestationClient = new AttestationClient(
1342
+ attestationUrl,
1343
+ attestorApiKey,
1344
+ );
1345
+ const attestationResponse = yield* Effect.tryPromise({
1346
+ try: () =>
1347
+ attestationClient.getAttestation(depositAddress, beneficiary),
1348
+ catch: (cause) => Effect.fail(cause as Error),
1349
+ }).pipe(Effect.retry(commonRetry));
1350
+
1351
+ // 3. Build redeem params
1352
+ const redeemParams = proofJsonToRedeemParams(proofJson, {
1353
+ lightClients: metadata.lightClients,
1354
+ nullifier: metadata.nullifier,
1355
+ value: metadata.value,
1356
+ beneficiary: metadata.beneficiary,
1357
+ attestedMessage: attestationResponse.attestedMessage,
1358
+ signature: attestationResponse.signature,
1359
+ });
1360
+
1361
+ // 4. Submit transaction
1362
+ const ZASSET_ABI = [
1363
+ {
1364
+ inputs: [
1365
+ { name: "proof", type: "uint256[8]" },
1366
+ { name: "commitments", type: "uint256[2]" },
1367
+ { name: "commitmentPok", type: "uint256[2]" },
1368
+ {
1369
+ name: "lightClients",
1370
+ type: "tuple[]",
1371
+ components: [
1372
+ { name: "clientId", type: "uint32" },
1373
+ { name: "height", type: "uint64" },
1374
+ ],
1375
+ },
1376
+ { name: "nullifier", type: "uint256" },
1377
+ { name: "value", type: "uint256" },
1378
+ { name: "beneficiary", type: "address" },
1379
+ { name: "attestedMessage", type: "bytes32" },
1380
+ { name: "signature", type: "bytes" },
1381
+ { name: "unwrap", type: "bool" },
1382
+ ],
1383
+ name: "redeem",
1384
+ outputs: [],
1385
+ stateMutability: "nonpayable",
1386
+ type: "function",
1387
+ },
1388
+ ] as const;
1389
+
1390
+ const txHash = yield* pipe(
1391
+ evm.writeContract({
1392
+ address: this.config.dstZAssetAddress,
1393
+ abi: ZASSET_ABI,
1394
+ functionName: "redeem",
1395
+ args: [
1396
+ redeemParams.proof,
1397
+ redeemParams.commitments,
1398
+ redeemParams.commitmentPok,
1399
+ redeemParams.lightClients,
1400
+ redeemParams.nullifier,
1401
+ redeemParams.value,
1402
+ redeemParams.beneficiary,
1403
+ redeemParams.attestedMessage,
1404
+ redeemParams.signature,
1405
+ unwrap,
1406
+ ],
1407
+ chain: walletClient.chain!,
1408
+ account: walletClient.account!,
1409
+ }),
1410
+ Effect.retry(commonRetry),
1411
+ );
1412
+
1413
+ return {
1414
+ txHash,
1415
+ proof: proofJson,
1416
+ metadata,
1417
+ } as const;
1418
+ }).pipe(
1419
+ Effect.provide(
1420
+ evm.PublicClient.Live({
1421
+ chain: options.walletClient.chain,
1422
+ transport: http(this.config.destinationRpcUrl),
1423
+ }),
1424
+ ),
1425
+ Effect.provideService(evm.WalletClient, {
1426
+ client: options.walletClient,
1427
+ account: options.walletClient.account!,
1428
+ chain: options.walletClient.chain!,
1429
+ }),
1430
+ Effect.runPromise,
1431
+ );
1432
+ }
1433
+
1434
+ /**
1435
+ * Full redeem flow: generate proof + get attestation + submit transaction
1436
+ *
1437
+ * This method orchestrates the entire redemption process:
1438
+ * 1. Generates a ZK proof via the prover server
1439
+ * 2. Gets attestation from the attestation service
1440
+ * 3. Submits the redeem transaction
1441
+ *
1442
+ * The wallet client must be connected to the destination chain.
1443
+ *
1444
+ * @returns Transaction hash, proof, and metadata
1445
+ */
1446
+ async unsafeRedeem(options: RedeemOptions): Promise<FullRedeemResult> {
1447
+ const {
1448
+ paymentKey,
1449
+ beneficiaries,
1450
+ beneficiary,
1451
+ amount,
1452
+ clientIds,
1453
+ selectedClientId,
1454
+ walletClient,
1455
+ unwrap = true,
1456
+ } = options;
1457
+
1458
+ // Resolve attestation URL and API key from options or stored config
1459
+ const attestationUrl = this.config.attestorUrl;
1460
+ const attestorApiKey = this.config.attestorApiKey;
1461
+
1462
+ if (!attestationUrl) {
1463
+ throw new Error(
1464
+ "Attestation URL must be provided either in redeem options or ClientOptions",
1465
+ );
1466
+ }
1467
+ if (!attestorApiKey) {
1468
+ throw new Error(
1469
+ "Attestation API key must be provided either in redeem options or ClientOptions",
1470
+ );
1471
+ }
1472
+
1473
+ if (!walletClient.account) {
1474
+ throw new Error("WalletClient must have an account");
1475
+ }
1476
+ if (!walletClient.chain) {
1477
+ throw new Error("WalletClient must have a chain configured");
1478
+ }
1479
+
1480
+ // 1. Generate proof
1481
+ const proofResult = await this.generateProof(
1482
+ paymentKey,
1483
+ beneficiaries,
1484
+ beneficiary,
1485
+ amount,
1486
+ clientIds,
1487
+ selectedClientId,
1488
+ );
1489
+
1490
+ if (
1491
+ !proofResult.proof.success ||
1492
+ !proofResult.proof.proofJson ||
1493
+ !proofResult.metadata
1494
+ ) {
1495
+ throw new Error(proofResult.proof.error ?? "Proof generation failed");
1496
+ }
1497
+
1498
+ const proofJson = parseProofJson(proofResult.proof.proofJson);
1499
+ const metadata = proofResult.metadata;
1500
+ const depositAddress = this.getDepositAddress(paymentKey, beneficiaries);
1501
+
1502
+ // 2. Get attestation
1503
+ const attestationClient = new AttestationClient(
1504
+ attestationUrl,
1505
+ attestorApiKey,
1506
+ );
1507
+ const attestationResponse = await attestationClient.getAttestation(
1508
+ depositAddress,
1509
+ beneficiary,
1510
+ );
1511
+
1512
+ // 3. Build redeem params
1513
+ const redeemParams = proofJsonToRedeemParams(proofJson, {
1514
+ lightClients: metadata.lightClients,
1515
+ nullifier: metadata.nullifier,
1516
+ value: metadata.value,
1517
+ beneficiary: metadata.beneficiary,
1518
+ attestedMessage: attestationResponse.attestedMessage,
1519
+ signature: attestationResponse.signature,
1520
+ });
1521
+
1522
+ // 4. Submit transaction
1523
+ const publicClient = createPublicClient({
1524
+ chain: walletClient.chain,
1525
+ transport: http(this.config.destinationRpcUrl),
1526
+ });
1527
+
1528
+ const ZASSET_ABI = [
1529
+ {
1530
+ inputs: [
1531
+ { name: "proof", type: "uint256[8]" },
1532
+ { name: "commitments", type: "uint256[2]" },
1533
+ { name: "commitmentPok", type: "uint256[2]" },
1534
+ {
1535
+ name: "lightClients",
1536
+ type: "tuple[]",
1537
+ components: [
1538
+ { name: "clientId", type: "uint32" },
1539
+ { name: "height", type: "uint64" },
1540
+ ],
1541
+ },
1542
+ { name: "nullifier", type: "uint256" },
1543
+ { name: "value", type: "uint256" },
1544
+ { name: "beneficiary", type: "address" },
1545
+ { name: "attestedMessage", type: "bytes32" },
1546
+ { name: "signature", type: "bytes" },
1547
+ { name: "unwrap", type: "bool" },
1548
+ ],
1549
+ name: "redeem",
1550
+ outputs: [],
1551
+ stateMutability: "nonpayable",
1552
+ type: "function",
1553
+ },
1554
+ ] as const;
1555
+
1556
+ const txHash = await walletClient
1557
+ .writeContractSync({
1558
+ address: this.config.dstZAssetAddress,
1559
+ abi: ZASSET_ABI,
1560
+ functionName: "redeem",
1561
+ args: [
1562
+ redeemParams.proof,
1563
+ redeemParams.commitments,
1564
+ redeemParams.commitmentPok,
1565
+ redeemParams.lightClients,
1566
+ redeemParams.nullifier,
1567
+ redeemParams.value,
1568
+ redeemParams.beneficiary,
1569
+ redeemParams.attestedMessage,
1570
+ redeemParams.signature,
1571
+ unwrap,
1572
+ ],
1573
+ chain: walletClient.chain,
1574
+ account: walletClient.account,
1575
+ })
1576
+ .then((x) => x.transactionHash);
1577
+
1578
+ const receipt = await publicClient.waitForTransactionReceipt({
1579
+ hash: txHash,
1580
+ });
1581
+ if (receipt.status === "reverted") {
1582
+ throw new Error(`Redeem transaction reverted: ${txHash}`);
1583
+ }
1584
+
1585
+ return {
1586
+ txHash,
1587
+ proof: proofJson,
1588
+ metadata,
1589
+ };
1590
+ }
1591
+
1592
+ /**
1593
+ * Pad beneficiaries array to exactly 4 addresses
1594
+ * Empty array = unbounded mode (all zeros, any beneficiary allowed)
1595
+ */
1596
+ private padBeneficiaries(
1597
+ beneficiaries: Address[],
1598
+ ): [Address, Address, Address, Address] {
1599
+ if (beneficiaries.length > 4) {
1600
+ throw new Error("Maximum 4 beneficiaries allowed");
1601
+ }
1602
+
1603
+ const padded: [Address, Address, Address, Address] = [
1604
+ beneficiaries[0] ?? ZERO_ADDRESS,
1605
+ beneficiaries[1] ?? ZERO_ADDRESS,
1606
+ beneficiaries[2] ?? ZERO_ADDRESS,
1607
+ beneficiaries[3] ?? ZERO_ADDRESS,
1608
+ ];
1609
+
1610
+ return padded;
1611
+ }
1612
+ }
1613
+
1614
+ /**
1615
+ * Wait for predicate on block height
1616
+ * @public
1617
+ */
1618
+ export const waitForBlockCondition = <T extends PublicClient>(
1619
+ publicClient: T,
1620
+ predicate: (blockNumber: bigint) => boolean,
1621
+ options?: { timeoutMs?: number | undefined },
1622
+ ): Promise<bigint> => {
1623
+ const timeoutMs = options?.timeoutMs ?? 60_000;
1624
+ return new Promise((resolve, reject) => {
1625
+ let done = false;
1626
+
1627
+ const unwatch = publicClient.watchBlockNumber({
1628
+ onBlockNumber(blockNumber) {
1629
+ if (done) return;
1630
+
1631
+ if (predicate(blockNumber)) {
1632
+ done = true;
1633
+ unwatch();
1634
+ resolve(blockNumber);
1635
+ }
1636
+ },
1637
+ onError(error) {
1638
+ if (done) return;
1639
+ done = true;
1640
+ unwatch();
1641
+ reject(error);
1642
+ },
1643
+ });
1644
+
1645
+ setTimeout(() => {
1646
+ if (done) return;
1647
+ done = true;
1648
+ unwatch();
1649
+ reject(new Error("Timed out waiting for block condition"));
1650
+ }, timeoutMs);
1651
+ });
1652
+ };