tapimo 0.0.2 → 0.1.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 (265) hide show
  1. package/README.md +11 -2
  2. package/dist/ApiKey.d.ts +12 -11
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +10 -12
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +139 -0
  7. package/dist/ApiKeys.d.ts.map +1 -0
  8. package/dist/ApiKeys.js +166 -0
  9. package/dist/ApiKeys.js.map +1 -0
  10. package/dist/App.d.ts +920 -910
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +36 -12
  13. package/dist/App.js.map +1 -1
  14. package/dist/Handler.d.ts +86 -0
  15. package/dist/Handler.d.ts.map +1 -0
  16. package/dist/Handler.js +83 -0
  17. package/dist/Handler.js.map +1 -0
  18. package/dist/Scope.d.ts +4 -1
  19. package/dist/Scope.d.ts.map +1 -1
  20. package/dist/Scope.js +5 -1
  21. package/dist/Scope.js.map +1 -1
  22. package/dist/Webhooks.d.ts +3 -5
  23. package/dist/Webhooks.d.ts.map +1 -1
  24. package/dist/Webhooks.js +9 -2
  25. package/dist/Webhooks.js.map +1 -1
  26. package/dist/admin/Access.d.ts +1 -3
  27. package/dist/admin/Access.d.ts.map +1 -1
  28. package/dist/admin/Access.js +1 -3
  29. package/dist/admin/Access.js.map +1 -1
  30. package/dist/admin/App.d.ts +98 -209
  31. package/dist/admin/App.d.ts.map +1 -1
  32. package/dist/admin/App.js +2 -4
  33. package/dist/admin/App.js.map +1 -1
  34. package/dist/admin/apps/api-keys.d.ts +7 -5
  35. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  36. package/dist/admin/apps/api-keys.js +8 -9
  37. package/dist/admin/apps/api-keys.js.map +1 -1
  38. package/dist/admin/apps/scopes.d.ts +3 -0
  39. package/dist/admin/apps/scopes.d.ts.map +1 -1
  40. package/dist/admin/apps/verified-tokens.d.ts +101 -215
  41. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  42. package/dist/admin/apps/verified-tokens.js +17 -38
  43. package/dist/admin/apps/verified-tokens.js.map +1 -1
  44. package/dist/admin/ui.generated.d.ts +1 -1
  45. package/dist/admin/ui.generated.js +1 -1
  46. package/dist/apps/balances.d.ts +2 -2
  47. package/dist/apps/receipts.d.ts +43 -43
  48. package/dist/apps/tokens.d.ts +454 -454
  49. package/dist/apps/transactions.d.ts +43 -43
  50. package/dist/apps/transfers.d.ts +29 -29
  51. package/dist/apps/webhooks.d.ts +5 -2
  52. package/dist/apps/webhooks.d.ts.map +1 -1
  53. package/dist/apps/webhooks.js +38 -34
  54. package/dist/apps/webhooks.js.map +1 -1
  55. package/dist/{Cli.d.ts → cli/Cli.d.ts} +13 -2
  56. package/dist/cli/Cli.d.ts.map +1 -0
  57. package/dist/cli/Cli.js +19 -0
  58. package/dist/cli/Cli.js.map +1 -0
  59. package/dist/cli/bin.d.ts +3 -0
  60. package/dist/cli/bin.d.ts.map +1 -0
  61. package/dist/cli/bin.js +4 -0
  62. package/dist/cli/bin.js.map +1 -0
  63. package/dist/cli/commands/admin.d.ts +15 -0
  64. package/dist/cli/commands/admin.d.ts.map +1 -0
  65. package/dist/cli/commands/admin.js +60 -0
  66. package/dist/cli/commands/admin.js.map +1 -0
  67. package/dist/cloudflare.d.ts +7 -0
  68. package/dist/cloudflare.d.ts.map +1 -1
  69. package/dist/cloudflare.js +9 -0
  70. package/dist/cloudflare.js.map +1 -1
  71. package/dist/db/Db.d.ts +103 -0
  72. package/dist/db/Db.d.ts.map +1 -0
  73. package/dist/db/Db.js +274 -0
  74. package/dist/db/Db.js.map +1 -0
  75. package/dist/db/Schema.d.ts +88 -0
  76. package/dist/db/Schema.d.ts.map +1 -0
  77. package/dist/db/Schema.js +130 -0
  78. package/dist/db/Schema.js.map +1 -0
  79. package/dist/db/tables/organizations.d.ts +79 -0
  80. package/dist/db/tables/organizations.d.ts.map +1 -0
  81. package/dist/db/tables/organizations.js +81 -0
  82. package/dist/db/tables/organizations.js.map +1 -0
  83. package/dist/db/tables/verifiedTokens.d.ts +53 -0
  84. package/dist/db/tables/verifiedTokens.d.ts.map +1 -0
  85. package/dist/db/tables/verifiedTokens.js +74 -0
  86. package/dist/db/tables/verifiedTokens.js.map +1 -0
  87. package/dist/db/tables/webhookDeliveries.d.ts +71 -0
  88. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -0
  89. package/dist/db/tables/webhookDeliveries.js +119 -0
  90. package/dist/db/tables/webhookDeliveries.js.map +1 -0
  91. package/dist/db/tables/webhookSubscriptions.d.ts +158 -0
  92. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -0
  93. package/dist/db/tables/webhookSubscriptions.js +272 -0
  94. package/dist/db/tables/webhookSubscriptions.js.map +1 -0
  95. package/dist/handlers/internal/executionError.d.ts +25 -0
  96. package/dist/handlers/internal/executionError.d.ts.map +1 -0
  97. package/dist/handlers/internal/executionError.js +184 -0
  98. package/dist/handlers/internal/executionError.js.map +1 -0
  99. package/dist/handlers/internal/multisig.d.ts +237 -0
  100. package/dist/handlers/internal/multisig.d.ts.map +1 -0
  101. package/dist/handlers/internal/multisig.js +567 -0
  102. package/dist/handlers/internal/multisig.js.map +1 -0
  103. package/dist/handlers/internal/requestListener.d.ts +124 -0
  104. package/dist/handlers/internal/requestListener.d.ts.map +1 -0
  105. package/dist/handlers/internal/requestListener.js +173 -0
  106. package/dist/handlers/internal/requestListener.js.map +1 -0
  107. package/dist/handlers/internal/sponsorship.d.ts +83 -0
  108. package/dist/handlers/internal/sponsorship.d.ts.map +1 -0
  109. package/dist/handlers/internal/sponsorship.js +103 -0
  110. package/dist/handlers/internal/sponsorship.js.map +1 -0
  111. package/dist/handlers/internal/types.d.ts +284 -0
  112. package/dist/handlers/internal/types.d.ts.map +1 -0
  113. package/dist/handlers/internal/types.js +2 -0
  114. package/dist/handlers/internal/types.js.map +1 -0
  115. package/dist/handlers/internal/utils.d.ts +34 -0
  116. package/dist/handlers/internal/utils.d.ts.map +1 -0
  117. package/dist/handlers/internal/utils.js +131 -0
  118. package/dist/handlers/internal/utils.js.map +1 -0
  119. package/dist/handlers/relay.d.ts +119 -0
  120. package/dist/handlers/relay.d.ts.map +1 -0
  121. package/dist/handlers/relay.js +1264 -0
  122. package/dist/handlers/relay.js.map +1 -0
  123. package/dist/index.d.ts +4 -2
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +4 -2
  126. package/dist/index.js.map +1 -1
  127. package/dist/internal/Auth.d.ts +16 -10
  128. package/dist/internal/Auth.d.ts.map +1 -1
  129. package/dist/internal/Auth.js +72 -12
  130. package/dist/internal/Auth.js.map +1 -1
  131. package/dist/internal/RpcSchema.d.ts +10 -0
  132. package/dist/internal/RpcSchema.d.ts.map +1 -0
  133. package/dist/internal/RpcSchema.js +2 -0
  134. package/dist/internal/RpcSchema.js.map +1 -0
  135. package/dist/internal/Store.d.ts +42 -3
  136. package/dist/internal/Store.d.ts.map +1 -1
  137. package/dist/internal/Store.js +65 -3
  138. package/dist/internal/Store.js.map +1 -1
  139. package/dist/internal/VerifiedTokens.d.ts +15 -34
  140. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  141. package/dist/internal/VerifiedTokens.js +115 -159
  142. package/dist/internal/VerifiedTokens.js.map +1 -1
  143. package/dist/internal/Webhooks.d.ts +49 -35
  144. package/dist/internal/Webhooks.d.ts.map +1 -1
  145. package/dist/internal/Webhooks.js +92 -205
  146. package/dist/internal/Webhooks.js.map +1 -1
  147. package/package.json +7 -2
  148. package/src/ApiKey.test.ts +3 -3
  149. package/src/ApiKey.ts +10 -12
  150. package/src/ApiKeys.test.ts +175 -0
  151. package/src/ApiKeys.ts +205 -0
  152. package/src/App.test-d.ts +1 -1
  153. package/src/App.test.ts +10768 -351
  154. package/src/App.ts +79 -36
  155. package/src/Client.test-d.ts +1 -1
  156. package/src/Handler.ts +154 -0
  157. package/src/Scope.ts +5 -1
  158. package/src/Webhooks.test.ts +8 -7
  159. package/src/Webhooks.ts +11 -5
  160. package/src/admin/Access.ts +1 -3
  161. package/src/admin/App.ts +19 -24
  162. package/src/admin/apps/api-keys.test.ts +19 -18
  163. package/src/admin/apps/api-keys.ts +8 -9
  164. package/src/admin/apps/scopes.test.ts +4 -0
  165. package/src/admin/apps/verified-tokens.test.ts +3 -30
  166. package/src/admin/apps/verified-tokens.ts +17 -36
  167. package/src/admin/ui.generated.ts +1 -1
  168. package/src/apps/balances.test.ts +4 -3
  169. package/src/apps/blocks.test.ts +1 -1
  170. package/src/apps/exchanges.test.ts +1 -1
  171. package/src/apps/indexer.test.ts +1 -1
  172. package/src/apps/receipts.test.ts +1 -1
  173. package/src/apps/rpc.test.ts +100 -10
  174. package/src/apps/tokenlist.test.ts +5 -7
  175. package/src/apps/tokens.test.ts +4 -3
  176. package/src/apps/transactions.test.ts +1 -1
  177. package/src/apps/transfers.test.ts +1 -1
  178. package/src/apps/verified-tokens.test.ts +24 -12
  179. package/src/apps/webhooks.test.ts +130 -143
  180. package/src/apps/webhooks.ts +45 -47
  181. package/src/{Cli.test.ts → cli/Cli.test.ts} +10 -9
  182. package/src/{Cli.ts → cli/Cli.ts} +16 -9
  183. package/src/cli/bin.ts +4 -0
  184. package/src/cli/commands/admin.test.ts +71 -0
  185. package/src/cli/commands/admin.ts +60 -0
  186. package/src/cloudflare.ts +10 -0
  187. package/src/db/Db.test.ts +84 -0
  188. package/src/db/Db.ts +383 -0
  189. package/src/db/Schema.ts +164 -0
  190. package/src/db/tables/organizations.test.ts +124 -0
  191. package/src/db/tables/organizations.ts +127 -0
  192. package/src/db/tables/verifiedTokens.ts +111 -0
  193. package/src/db/tables/webhookDeliveries.ts +159 -0
  194. package/src/db/tables/webhookSubscriptions.ts +358 -0
  195. package/src/handlers/internal/executionError.ts +205 -0
  196. package/src/handlers/internal/multisig.localnet.test.ts +146 -0
  197. package/src/handlers/internal/multisig.test.ts +744 -0
  198. package/src/handlers/internal/multisig.ts +858 -0
  199. package/src/handlers/internal/relay.localnet.test.ts +2035 -0
  200. package/src/handlers/internal/requestListener.ts +273 -0
  201. package/src/handlers/internal/sponsorship.ts +186 -0
  202. package/src/handlers/internal/types.ts +378 -0
  203. package/src/handlers/internal/utils.ts +153 -0
  204. package/src/handlers/relay.ts +1605 -0
  205. package/src/index.ts +4 -2
  206. package/src/internal/Auth.test.ts +21 -21
  207. package/src/internal/Auth.ts +92 -19
  208. package/src/internal/RateLimit.test.ts +1 -1
  209. package/src/internal/RpcSchema.ts +10 -0
  210. package/src/internal/Store.test.ts +98 -10
  211. package/src/internal/Store.ts +112 -6
  212. package/src/internal/VerifiedTokens.test.ts +99 -115
  213. package/src/internal/VerifiedTokens.ts +123 -175
  214. package/src/internal/Webhooks.test.ts +107 -107
  215. package/src/internal/Webhooks.ts +102 -220
  216. package/dist/ApiKeySource.d.ts +0 -166
  217. package/dist/ApiKeySource.d.ts.map +0 -1
  218. package/dist/ApiKeySource.js +0 -234
  219. package/dist/ApiKeySource.js.map +0 -1
  220. package/dist/Cli.d.ts.map +0 -1
  221. package/dist/Cli.js +0 -13
  222. package/dist/Cli.js.map +0 -1
  223. package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +0 -1
  224. package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
  225. package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
  226. package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
  227. package/seeds/31318/verified-tokens.json +0 -30
  228. package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
  229. package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +0 -1
  230. package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +0 -1
  231. package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +0 -4
  232. package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +0 -30
  233. package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +0 -1
  234. package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +0 -1
  235. package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +0 -1
  236. package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +0 -11
  237. package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +0 -1
  238. package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +0 -1
  239. package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
  240. package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +0 -1
  241. package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
  242. package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +0 -1
  243. package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +0 -5
  244. package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +0 -1
  245. package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +0 -1
  246. package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +0 -1
  247. package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +0 -1
  248. package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +0 -1
  249. package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +0 -1
  250. package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +0 -1
  251. package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +0 -1
  252. package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
  253. package/seeds/4217/verified-tokens.json +0 -177
  254. package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
  255. package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
  256. package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
  257. package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
  258. package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +0 -1
  259. package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +0 -1
  260. package/seeds/42431/verified-tokens.json +0 -44
  261. package/seeds/openrpc/README.md +0 -27
  262. package/seeds/openrpc/execution-apis.json +0 -8989
  263. package/src/ApiKeySource.test-d.ts +0 -11
  264. package/src/ApiKeySource.test.ts +0 -295
  265. package/src/ApiKeySource.ts +0 -303
@@ -0,0 +1,858 @@
1
+ import { Hex, Json, RpcResponse, Signature } from 'ox'
2
+ import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
3
+ import type { Address, Client } from 'viem'
4
+ import type { LocalAccount } from 'viem/accounts'
5
+ import { Transaction } from 'viem/tempo'
6
+
7
+ import * as core_Store from '../../internal/Store.js'
8
+ import * as Sponsorship from './sponsorship.js'
9
+ import * as Utils from './utils.js'
10
+
11
+ /** Default submission-claim TTL in milliseconds. */
12
+ export const defaultClaimTtl = 30_000
13
+
14
+ /** Returns whether a raw transaction carries a native multisig signature. */
15
+ export function isMultisigTransaction(serialized: Hex.Hex) {
16
+ if (!Utils.isSerializedTempoTransaction(serialized)) return false
17
+ try {
18
+ const transaction = Transaction.deserialize(serialized as never)
19
+ return isMultisigSignature((transaction as { signature?: unknown }).signature)
20
+ } catch {
21
+ return false
22
+ }
23
+ }
24
+
25
+ /** Handles a native multisig raw transaction submission. */
26
+ export async function handleRawTransaction(
27
+ options: handleRawTransaction.Options,
28
+ ): Promise<handleRawTransaction.ReturnType> {
29
+ const result = await collect(options)
30
+ if (result.status.status === 'pending' || result.status.status === 'submitting')
31
+ return result.status.operation.id
32
+ if (result.status.status === 'submitted')
33
+ return result.status.operation.submittedHash ?? result.status.operation.id
34
+ if (options.method === 'eth_sendRawTransactionSync') return result.broadcastResult
35
+ return result.status.operation.submittedHash!
36
+ }
37
+
38
+ export declare namespace handleRawTransaction {
39
+ /** Options for handling a raw multisig transaction submission. */
40
+ type Options = collect.Options
41
+
42
+ /** Raw transaction handler return value. */
43
+ // oxlint-disable-next-line typescript/no-redundant-type-constituents
44
+ type ReturnType = Hex.Hex | unknown
45
+ }
46
+
47
+ async function collect(options: collect.Options): Promise<collect.ReturnType> {
48
+ const {
49
+ claimTtl = defaultClaimTtl,
50
+ finalize = 'sync',
51
+ getClient,
52
+ method,
53
+ request,
54
+ resolveConfig,
55
+ sponsor,
56
+ store,
57
+ } = options
58
+ const serialized = request.params?.[0]
59
+ if (!Utils.isSerializedTempoTransaction(serialized))
60
+ throw new RpcResponse.InvalidParamsError({
61
+ message: 'Only Tempo (0x76/0x78) transactions are supported.',
62
+ })
63
+
64
+ const input = parse(serialized)
65
+ const pending = await store.get(input.id)
66
+ const config = await resolveValidatedConfig({
67
+ account: input.account,
68
+ chainId: input.chainId,
69
+ genesisConfigId: input.genesisConfigId,
70
+ init: input.init,
71
+ pending,
72
+ resolveConfig,
73
+ })
74
+ if (!config)
75
+ throw new RpcResponse.InvalidParamsError({
76
+ message:
77
+ 'Multisig config is required to collect approvals. Provide it in the bootstrap transaction or configure `multisig.resolveConfig`.',
78
+ })
79
+ const initConfig = resolveInitConfig({
80
+ config,
81
+ genesisConfigId: input.genesisConfigId,
82
+ init: input.init,
83
+ nonce: input.nonce,
84
+ pending,
85
+ })
86
+
87
+ const now = Date.now()
88
+ let operation: Operation = {
89
+ account: input.account,
90
+ chainId: input.chainId,
91
+ claimExpiresAt: pending?.claimExpiresAt,
92
+ config,
93
+ createdAt: pending?.createdAt ?? now,
94
+ genesisConfigId: input.genesisConfigId,
95
+ id: input.id,
96
+ init: pending?.init || !!initConfig,
97
+ initConfig,
98
+ payload: input.payload,
99
+ signatures: mergeSignatures([...(pending?.signatures ?? []), ...input.signatures]),
100
+ status: pending?.status === 'submitted' ? 'submitted' : 'pending',
101
+ submittedHash: pending?.submittedHash,
102
+ transaction: serialized as Hex.Hex,
103
+ updatedAt: now,
104
+ }
105
+
106
+ let approvals = getApprovals({
107
+ account: input.account,
108
+ config,
109
+ genesisConfigId: input.genesisConfigId,
110
+ payload: input.payload,
111
+ signatures: operation.signatures,
112
+ })
113
+ if (operation.status === 'submitted')
114
+ return {
115
+ broadcastResult: operation.submittedHash,
116
+ status: { operation, status: 'submitted' },
117
+ }
118
+
119
+ if (approvals.weight < approvals.threshold) {
120
+ operation = await store.savePending(operation)
121
+ approvals = getApprovals({
122
+ account: operation.account,
123
+ config,
124
+ genesisConfigId: operation.genesisConfigId,
125
+ payload: operation.payload,
126
+ signatures: operation.signatures,
127
+ })
128
+ if (approvals.weight < approvals.threshold)
129
+ return { status: { operation, status: operation.status } }
130
+ }
131
+
132
+ const claim = await store.claimSubmission(operation, { ttl: claimTtl })
133
+ if (claim.status !== 'claimed') return { status: claim }
134
+
135
+ operation = claim.operation
136
+ approvals = getApprovals({
137
+ account: operation.account,
138
+ config,
139
+ genesisConfigId: operation.genesisConfigId,
140
+ payload: operation.payload,
141
+ signatures: operation.signatures,
142
+ })
143
+ const client = getClient(operation.chainId)
144
+ const final = await serializeFinal({
145
+ account: operation.account,
146
+ genesisConfigId: operation.genesisConfigId,
147
+ init: operation.initConfig ?? initConfig,
148
+ signatures: approvals.signatures,
149
+ transaction: input.transaction,
150
+ })
151
+ const broadcastMethod =
152
+ method === 'eth_sendRawTransaction' && finalize === 'sync'
153
+ ? 'eth_sendRawTransactionSync'
154
+ : method
155
+ const feePayerState = (() => {
156
+ try {
157
+ const transaction = Transaction.deserialize(final as never) as Record<string, unknown>
158
+ // No fee-payer field means finalized transaction does not request sponsorship.
159
+ if (!('feePayerSignature' in transaction)) return {}
160
+ // Preserve fee-token/signature marker for sponsorship decision below.
161
+ return {
162
+ feeToken:
163
+ typeof transaction['feeToken'] === 'string'
164
+ ? (transaction['feeToken'] as Address)
165
+ : undefined,
166
+ signature: transaction['feePayerSignature'],
167
+ }
168
+ } catch {
169
+ // Decode failure falls back to direct broadcast.
170
+ return {}
171
+ }
172
+ })()
173
+ const sponsorFeeToken = await (async () => {
174
+ // No sponsor configured, so finalized multisig transaction broadcasts as-is.
175
+ if (!sponsor) return undefined
176
+ // Already fee-payer signed transactions must not be rewritten by this relay.
177
+ if (feePayerState.signature !== null) return undefined
178
+ // Explicit relay fee token wins when the transaction requests sponsorship.
179
+ if (sponsor.feeToken) return sponsor.feeToken
180
+ // `null` requests relay-added fee-payer signature.
181
+ return await sponsor.resolveFeeToken?.(operation.chainId)
182
+ })()
183
+ const shouldSponsor = (() => {
184
+ // No sponsor configured, so no sponsorship path to take.
185
+ if (!sponsor) return false
186
+ // `null` requests relay-added fee-payer signature.
187
+ if (feePayerState.signature === null) return true
188
+ // Missing or already-present fee-payer signatures broadcast unchanged.
189
+ return false
190
+ })()
191
+ const result = await (async () => {
192
+ // Sponsorship enabled, so sponsor adds/signs fee-payer fields.
193
+ if (shouldSponsor && sponsor)
194
+ return await Sponsorship.handleRawTransaction({
195
+ account: sponsor.account,
196
+ feeToken: sponsorFeeToken,
197
+ getFeeToken: sponsor.resolveFeeToken
198
+ ? async (chainId) => await sponsor.resolveFeeToken?.(chainId)
199
+ : undefined,
200
+ getClient,
201
+ method: broadcastMethod,
202
+ request: { params: [final] },
203
+ sender: operation.account,
204
+ validate: sponsor.validate,
205
+ })
206
+ // Otherwise, multisig transaction is final and can broadcast directly.
207
+ return await client.request({
208
+ method: broadcastMethod,
209
+ params: [final],
210
+ } as never)
211
+ })()
212
+ const submittedHash = (() => {
213
+ if (typeof result === 'string') return result as Hex.Hex
214
+ if (result && typeof result === 'object' && 'transactionHash' in result) {
215
+ const response = result as { transactionHash?: unknown }
216
+ if (typeof response.transactionHash === 'string') return response.transactionHash as Hex.Hex
217
+ }
218
+ throw new Error('Expected transaction hash in multisig broadcast result.')
219
+ })()
220
+ const submitted = await store.setSubmitted(operation.id, submittedHash)
221
+
222
+ return {
223
+ broadcastResult: result,
224
+ status: { operation: submitted, status: 'claimed' },
225
+ }
226
+ }
227
+
228
+ /** Resolves a multisig operation id for standard transaction lookup methods. */
229
+ export async function handleGetTransaction(
230
+ options: handleGetTransaction.Options,
231
+ ): Promise<handleGetTransaction.ReturnType> {
232
+ const id = options.request.params?.[0]
233
+ if (typeof id !== 'string') return undefined
234
+
235
+ const operation = await options.store.get(id as Hex.Hex)
236
+ if (!operation) return undefined
237
+ if (operation.status !== 'submitted' || !operation.submittedHash) return { result: null }
238
+
239
+ const result = await options.getClient(operation.chainId).request({
240
+ method: options.method,
241
+ params: [operation.submittedHash],
242
+ } as never)
243
+ return { result }
244
+ }
245
+
246
+ export declare namespace handleGetTransaction {
247
+ /** Options for resolving a multisig operation id through transaction lookup methods. */
248
+ type Options = {
249
+ /** Client resolver keyed by transaction `chainId`. */
250
+ getClient: (chainId?: number) => Client
251
+ /** Standard transaction lookup method to handle. */
252
+ method: 'eth_getTransactionByHash' | 'eth_getTransactionReceipt'
253
+ /** Incoming JSON-RPC request. */
254
+ request: { params?: readonly unknown[] | undefined }
255
+ /** Pending multisig operation store. */
256
+ store: Store
257
+ }
258
+
259
+ /** Transaction lookup result wrapper. */
260
+ type ReturnType = { result: unknown } | undefined
261
+ }
262
+
263
+ declare namespace collect {
264
+ type Options = {
265
+ /** Submission claim TTL in milliseconds. @default 30000 */
266
+ claimTtl?: number | undefined
267
+ /** How to broadcast once quorum is met. @default 'sync' */
268
+ finalize?: 'submitted' | 'sync' | undefined
269
+ /** Client resolver keyed by transaction `chainId`. */
270
+ getClient: (chainId?: number) => Client
271
+ /** Raw transaction method to handle. */
272
+ method: 'eth_sendRawTransaction' | 'eth_sendRawTransactionSync'
273
+ /** Incoming JSON-RPC request. */
274
+ request: { params?: readonly unknown[] | undefined }
275
+ /** Resolves the genesis multisig config for quorum checks. */
276
+ resolveConfig?: ResolveConfig | undefined
277
+ /** Optional fee payer used to sponsor finalized multisig transactions. */
278
+ sponsor?: Sponsor | undefined
279
+ /** Pending multisig operation store. */
280
+ store: Store
281
+ }
282
+
283
+ type ReturnType = {
284
+ /** Raw broadcast result once quorum is met and this caller claimed submission. */
285
+ broadcastResult?: unknown
286
+ /** Structured operation claim status. */
287
+ status: ClaimSubmissionResult | { operation: Operation; status: 'claimed' }
288
+ }
289
+ }
290
+
291
+ /** Returns a stored native multisig operation status. */
292
+ export async function getStatus(options: getStatus.Options): Promise<Status | null> {
293
+ const operation = await options.store.get(options.id)
294
+ if (!operation) return null
295
+ const config = await resolveValidatedConfig({
296
+ account: operation.account,
297
+ chainId: operation.chainId,
298
+ genesisConfigId: operation.genesisConfigId,
299
+ pending: operation,
300
+ resolveConfig: options.resolveConfig,
301
+ })
302
+ if (!config)
303
+ return {
304
+ account: operation.account,
305
+ chainId: operation.chainId,
306
+ genesisConfigId: operation.genesisConfigId,
307
+ id: operation.id,
308
+ signatures: operation.signatures.length,
309
+ status: operation.status,
310
+ submittedHash: operation.submittedHash,
311
+ }
312
+ const approvals = getApprovals({
313
+ account: operation.account,
314
+ config,
315
+ genesisConfigId: operation.genesisConfigId,
316
+ payload: operation.payload,
317
+ signatures: operation.signatures,
318
+ })
319
+ return toStatus({ approvals, operation })
320
+ }
321
+
322
+ export declare namespace getStatus {
323
+ /** Options for reading one multisig operation status. */
324
+ type Options = {
325
+ /** Operation id returned by a pending multisig submission. */
326
+ id: Hex.Hex
327
+ /** Resolves the genesis multisig config for quorum status. */
328
+ resolveConfig?: ResolveConfig | undefined
329
+ /** Pending multisig operation store. */
330
+ store: Store
331
+ }
332
+ }
333
+
334
+ /** Returns pending native multisig operation statuses for an account. */
335
+ export async function listStatuses(options: listStatuses.Options): Promise<readonly Status[]> {
336
+ const operations = await options.store.listPendingByAddress(options.account)
337
+ return await Promise.all(
338
+ operations.map(async (operation) => {
339
+ const config = await resolveValidatedConfig({
340
+ account: operation.account,
341
+ chainId: operation.chainId,
342
+ genesisConfigId: operation.genesisConfigId,
343
+ pending: operation,
344
+ resolveConfig: options.resolveConfig,
345
+ })
346
+ if (!config)
347
+ return {
348
+ account: operation.account,
349
+ chainId: operation.chainId,
350
+ genesisConfigId: operation.genesisConfigId,
351
+ id: operation.id,
352
+ signatures: operation.signatures.length,
353
+ status: operation.status,
354
+ submittedHash: operation.submittedHash,
355
+ }
356
+ const approvals = getApprovals({
357
+ account: operation.account,
358
+ config,
359
+ genesisConfigId: operation.genesisConfigId,
360
+ payload: operation.payload,
361
+ signatures: operation.signatures,
362
+ })
363
+ return toStatus({ approvals, operation })
364
+ }),
365
+ )
366
+ }
367
+
368
+ export declare namespace listStatuses {
369
+ /** Options for listing multisig operation statuses. */
370
+ type Options = {
371
+ /** Native multisig account address. */
372
+ account: Address
373
+ /** Resolves the genesis multisig config for quorum status. */
374
+ resolveConfig?: ResolveConfig | undefined
375
+ /** Pending multisig operation store. */
376
+ store: Store
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Creates a multisig operation store backed by a Tempo API state store
382
+ * (`Store.memory()`, a static-name Durable Object store, …).
383
+ *
384
+ * Approval merges and submission claims go through `Store.update`, so the
385
+ * backing store needs the atomic `swap` compare-and-swap to serialize
386
+ * concurrent writers (the get + put fallback is dev-only). It must also be
387
+ * enumerable — `listPendingByAddress` scans a key prefix — so sharded Durable
388
+ * Object stores and caches do not qualify.
389
+ */
390
+ export function fromStore(state: core_Store.State): Store {
391
+ const key = (id: Hex.Hex) => `multisig:op:${id.toLowerCase()}`
392
+ const read = (raw: null | string) => (raw ? (Json.parse(raw) as Operation) : undefined)
393
+
394
+ return {
395
+ async claimSubmission(operation, options) {
396
+ let result: ClaimSubmissionResult | undefined
397
+ await core_Store.update(state, key(operation.id), (raw) => {
398
+ const now = Date.now()
399
+ const existing = read(raw)
400
+ if (existing?.status === 'submitted') {
401
+ result = { operation: existing, status: 'submitted' }
402
+ return raw as string
403
+ }
404
+ if (existing?.status === 'submitting' && (existing.claimExpiresAt ?? 0) > now) {
405
+ result = { operation: existing, status: 'submitting' }
406
+ return raw as string
407
+ }
408
+ const claimed = mergeOperation(existing, operation, {
409
+ claimExpiresAt: now + options.ttl,
410
+ status: 'submitting',
411
+ updatedAt: now,
412
+ })
413
+ result = { operation: claimed, status: 'claimed' }
414
+ return Json.stringify(claimed)
415
+ })
416
+ return result as ClaimSubmissionResult
417
+ },
418
+ async get(id) {
419
+ return read(await state.get(key(id)))
420
+ },
421
+ async listPendingByAddress(address) {
422
+ const { keys } = await state.list({ prefix: 'multisig:op:' })
423
+ const operations = await Promise.all(keys.map(({ name }) => state.get(name)))
424
+ return operations
425
+ .map(read)
426
+ .filter((operation) => operation !== undefined)
427
+ .filter(
428
+ (operation) =>
429
+ operation.account.toLowerCase() === address.toLowerCase() &&
430
+ operation.status !== 'submitted',
431
+ )
432
+ },
433
+ async savePending(operation) {
434
+ let saved: Operation | undefined
435
+ await core_Store.update(state, key(operation.id), (raw) => {
436
+ const now = Date.now()
437
+ const existing = read(raw)
438
+ if (existing?.status === 'submitted') {
439
+ saved = existing
440
+ return raw as string
441
+ }
442
+ saved = mergeOperation(existing, operation, {
443
+ status:
444
+ existing?.status === 'submitting' && (existing.claimExpiresAt ?? 0) > now
445
+ ? 'submitting'
446
+ : 'pending',
447
+ updatedAt: now,
448
+ })
449
+ return Json.stringify(saved)
450
+ })
451
+ return saved as Operation
452
+ },
453
+ async setSubmitted(id, submittedHash) {
454
+ let submitted: Operation | undefined
455
+ await core_Store.update(state, key(id), (raw) => {
456
+ const existing = read(raw)
457
+ if (!existing) throw new Error('Cannot mark an unknown multisig operation submitted.')
458
+ submitted = {
459
+ ...existing,
460
+ claimExpiresAt: undefined,
461
+ status: 'submitted',
462
+ submittedHash,
463
+ updatedAt: Date.now(),
464
+ } satisfies Operation
465
+ return Json.stringify(submitted)
466
+ })
467
+ return submitted as Operation
468
+ },
469
+ }
470
+ }
471
+
472
+ /** Creates an in-memory multisig operation store for development and tests only. */
473
+ export function memoryStore(): Store {
474
+ return fromStore(core_Store.memory())
475
+ }
476
+
477
+ /** Native multisig relay options. */
478
+ export type Options = {
479
+ /** Submission claim TTL in milliseconds. @default 30000 */
480
+ claimTtl?: number | undefined
481
+ /** How to broadcast once quorum is met. @default 'sync' */
482
+ finalize?: 'submitted' | 'sync' | undefined
483
+ /** Resolves the genesis multisig config for quorum checks. */
484
+ resolveConfig?: ResolveConfig | undefined
485
+ /**
486
+ * State store holding pending multisig operations. Needs the store's atomic
487
+ * `swap` for correctness (approval merges and submission claims serialize
488
+ * through it) and must be enumerable, so sharded Durable Object stores and
489
+ * caches do not qualify.
490
+ */
491
+ store: core_Store.State
492
+ }
493
+
494
+ /** Resolves the genesis multisig config for quorum checks. */
495
+ export type ResolveConfig = (request: {
496
+ /** Native multisig account address. */
497
+ account: Address
498
+ /** Transaction chain id. */
499
+ chainId: number
500
+ /** Permanent genesis config id. */
501
+ genesisConfigId: Hex.Hex
502
+ /** Bootstrap config carried by the transaction, when present. */
503
+ init?: MultisigConfig.Config | undefined
504
+ }) => MultisigConfig.Config | Promise<MultisigConfig.Config | undefined> | undefined
505
+
506
+ /** Optional fee payer used to sponsor finalized multisig transactions. */
507
+ export type Sponsor = {
508
+ /** Account used as the fee payer. */
509
+ account: LocalAccount
510
+ /** Fee token to set before fee payer signing. */
511
+ feeToken?: Address | undefined
512
+ /** Resolves the default fee token for raw sponsorship. */
513
+ resolveFeeToken?:
514
+ | ((chainId: number) => Address | Promise<Address | undefined> | undefined)
515
+ | undefined
516
+ /** Optional sponsorship approval callback. */
517
+ validate?: Sponsorship.handleRawTransaction.Options['validate']
518
+ }
519
+
520
+ /** Storage for native multisig operation approvals and submission claims. */
521
+ export type Store = {
522
+ /** Atomically claims submission for a quorum-ready operation. Only a `claimed` result may broadcast. */
523
+ claimSubmission: (
524
+ operation: Operation,
525
+ options: claimSubmission.Options,
526
+ ) => Promise<ClaimSubmissionResult>
527
+ /** Reads a stored operation. */
528
+ get: (id: Hex.Hex) => Promise<Operation | undefined>
529
+ /** Lists non-submitted operations for a native multisig account address. */
530
+ listPendingByAddress: (address: Address) => Promise<readonly Operation[]>
531
+ /** Saves a pending operation, preserving already-stored approvals. */
532
+ savePending: (operation: Operation) => Promise<Operation>
533
+ /** Marks a claimed operation submitted with its real transaction hash. */
534
+ setSubmitted: (id: Hex.Hex, submittedHash: Hex.Hex) => Promise<Operation>
535
+ }
536
+
537
+ /** `Store.claimSubmission` types. */
538
+ export declare namespace claimSubmission {
539
+ /** Options for claiming submission. */
540
+ type Options = {
541
+ /** Claim TTL in milliseconds. */
542
+ ttl: number
543
+ }
544
+ }
545
+
546
+ /** Result of atomically claiming a multisig operation for submission. */
547
+ export type ClaimSubmissionResult =
548
+ | {
549
+ /** Operation this caller may broadcast. */
550
+ operation: Operation
551
+ /** Claim status. */
552
+ status: 'claimed'
553
+ }
554
+ | {
555
+ /** Current stored operation. */
556
+ operation: Operation
557
+ /** Current non-claimed status. */
558
+ status: 'pending' | 'submitting' | 'submitted'
559
+ }
560
+
561
+ /** Stored native multisig operation. */
562
+ export type Operation = {
563
+ /** Native multisig account address. */
564
+ account: Address
565
+ /** Transaction chain id. */
566
+ chainId: number
567
+ /** Submission claim expiry timestamp in milliseconds. */
568
+ claimExpiresAt?: number | undefined
569
+ /** Resolved genesis config used for approval checks. */
570
+ config?: MultisigConfig.Config | undefined
571
+ /** Creation timestamp in milliseconds. */
572
+ createdAt: number
573
+ /** Permanent genesis config id. */
574
+ genesisConfigId: Hex.Hex
575
+ /** Deterministic operation id. */
576
+ id: Hex.Hex
577
+ /** Whether the finalized transaction must carry the genesis init config. */
578
+ init?: boolean | undefined
579
+ /** Genesis init config to attach when finalizing a bootstrap transaction. */
580
+ initConfig?: MultisigConfig.Config | undefined
581
+ /** Unsigned Tempo transaction sign payload. */
582
+ payload: Hex.Hex
583
+ /** Collected owner approval signatures. */
584
+ signatures: readonly Hex.Hex[]
585
+ /** Operation state. */
586
+ status: 'pending' | 'submitting' | 'submitted'
587
+ /** Transaction hash once submitted. */
588
+ submittedHash?: Hex.Hex | undefined
589
+ /** Last submitted serialized transaction carrying this operation. */
590
+ transaction: Hex.Hex
591
+ /** Last update timestamp in milliseconds. */
592
+ updatedAt: number
593
+ }
594
+
595
+ /** Native multisig operation status. */
596
+ export type Status = {
597
+ /** Native multisig account address. */
598
+ account: Address
599
+ /** Transaction chain id. */
600
+ chainId: number
601
+ /** Permanent genesis config id. */
602
+ genesisConfigId: Hex.Hex
603
+ /** Deterministic operation id. */
604
+ id: Hex.Hex
605
+ /** Number of collected owner approvals. */
606
+ signatures: number
607
+ /** Operation state. */
608
+ status: Operation['status']
609
+ /** Submitted transaction hash. */
610
+ submittedHash?: Hex.Hex | undefined
611
+ /** Required owner weight. */
612
+ threshold?: number | undefined
613
+ /** Collected owner weight. */
614
+ weight?: number | undefined
615
+ }
616
+
617
+ function parse(serialized: Hex.Hex) {
618
+ const transaction = Transaction.deserialize(serialized as never) as Record<string, unknown>
619
+ const signature = transaction['signature']
620
+ if (!isMultisigSignature(signature))
621
+ throw new RpcResponse.InvalidParamsError({
622
+ message: 'Transaction does not contain a native multisig signature.',
623
+ })
624
+
625
+ const { signature: _, ...unsigned } = transaction
626
+ const payload = TxEnvelopeTempo.getSignPayload(TxEnvelopeTempo.from(unsigned as never))
627
+ const chainId = Number(transaction['chainId'])
628
+ const id = MultisigConfig.getSignPayload({
629
+ account: signature.account,
630
+ genesisConfigId: signature.genesisConfigId,
631
+ payload,
632
+ })
633
+ const init = signature.init ? MultisigConfig.from(signature.init) : undefined
634
+ const nonce = typeof transaction['nonce'] === 'number' ? transaction['nonce'] : undefined
635
+
636
+ return {
637
+ account: signature.account,
638
+ chainId,
639
+ genesisConfigId: signature.genesisConfigId,
640
+ id,
641
+ init,
642
+ nonce,
643
+ payload,
644
+ signatures: signature.signatures.map((value) => SignatureEnvelope.serialize(value)),
645
+ transaction,
646
+ }
647
+ }
648
+
649
+ function isMultisigSignature(value: unknown): value is SignatureEnvelope.Multisig {
650
+ return (
651
+ typeof value === 'object' &&
652
+ value !== null &&
653
+ (value as { type?: unknown }).type === 'multisig' &&
654
+ typeof (value as { account?: unknown }).account === 'string' &&
655
+ typeof (value as { genesisConfigId?: unknown }).genesisConfigId === 'string' &&
656
+ Array.isArray((value as { signatures?: unknown }).signatures)
657
+ )
658
+ }
659
+
660
+ async function resolveValidatedConfig(options: {
661
+ account: Address
662
+ chainId: number
663
+ genesisConfigId: Hex.Hex
664
+ init?: MultisigConfig.Config | undefined
665
+ pending?: Operation | undefined
666
+ resolveConfig?: ResolveConfig | undefined
667
+ }) {
668
+ const { account, chainId, genesisConfigId, init, pending, resolveConfig } = options
669
+ const config =
670
+ (await resolveConfig?.({ account, chainId, genesisConfigId, init })) ?? pending?.config ?? init
671
+ if (!config) return undefined
672
+
673
+ if (init && !isGenesisConfig(init, genesisConfigId))
674
+ throw new RpcResponse.InvalidParamsError({
675
+ message: 'Bootstrap multisig init config does not match genesis config id.',
676
+ })
677
+
678
+ const normalized = MultisigConfig.from(config)
679
+ const account_expected = MultisigConfig.getAddress({ genesisConfigId })
680
+ if (account_expected.toLowerCase() !== account.toLowerCase())
681
+ throw new RpcResponse.InvalidParamsError({
682
+ message: 'Resolved multisig config does not match account or genesis config id.',
683
+ })
684
+ return normalized
685
+ }
686
+
687
+ function resolveInitConfig(options: {
688
+ config: MultisigConfig.Config
689
+ genesisConfigId: Hex.Hex
690
+ init?: MultisigConfig.Config | undefined
691
+ nonce?: number | undefined
692
+ pending?: Operation | undefined
693
+ }) {
694
+ const { config, genesisConfigId, init, nonce, pending } = options
695
+ if (pending?.initConfig) return pending.initConfig
696
+ if (pending?.init && pending.config && isGenesisConfig(pending.config, genesisConfigId))
697
+ return pending.config
698
+ if (init) return init
699
+ if (nonce === 0 && isGenesisConfig(config, genesisConfigId)) return config
700
+ return undefined
701
+ }
702
+
703
+ function isGenesisConfig(config: MultisigConfig.Config, genesisConfigId: Hex.Hex) {
704
+ return MultisigConfig.toId(config).toLowerCase() === genesisConfigId.toLowerCase()
705
+ }
706
+
707
+ function mergeSignatures(signatures: readonly Hex.Hex[]) {
708
+ const seen = new Set<string>()
709
+ const out: Hex.Hex[] = []
710
+ for (const signature of signatures) {
711
+ const key = signature.toLowerCase()
712
+ if (seen.has(key)) continue
713
+ seen.add(key)
714
+ out.push(signature)
715
+ }
716
+ return out
717
+ }
718
+
719
+ function getApprovals(options: {
720
+ account: Address
721
+ config: MultisigConfig.Config
722
+ genesisConfigId: Hex.Hex
723
+ payload: Hex.Hex
724
+ signatures: readonly Hex.Hex[]
725
+ }) {
726
+ const { account, config, genesisConfigId, payload } = options
727
+ const digest = MultisigConfig.getSignPayload({ account, genesisConfigId, payload })
728
+ const owners = new Map(
729
+ config.owners.map((owner) => [
730
+ owner.owner.toLowerCase(),
731
+ { address: owner.owner, weight: Number(owner.weight) },
732
+ ]),
733
+ )
734
+ const seen = new Set<string>()
735
+ const signatures: Hex.Hex[] = []
736
+ let weight = 0
737
+
738
+ for (const value of options.signatures) {
739
+ const signature = SignatureEnvelope.from(value)
740
+ const owner = SignatureEnvelope.extractAddress({ payload: digest, signature })
741
+ const key = owner.toLowerCase()
742
+ const configured = owners.get(key)
743
+ if (!configured)
744
+ throw new RpcResponse.InvalidParamsError({ message: `Signature from non-owner ${owner}.` })
745
+ if (seen.has(key)) continue
746
+ const valid = (() => {
747
+ try {
748
+ return SignatureEnvelope.verify(signature, { payload: digest, address: configured.address })
749
+ } catch {
750
+ return false
751
+ }
752
+ })()
753
+ if (!valid)
754
+ throw new RpcResponse.InvalidParamsError({
755
+ message: `Invalid signature from owner ${configured.address}.`,
756
+ })
757
+ seen.add(key)
758
+ signatures.push(SignatureEnvelope.serialize(signature))
759
+ weight += configured.weight
760
+ }
761
+
762
+ return { signatures, threshold: Number(config.threshold), weight }
763
+ }
764
+
765
+ async function serializeFinal(options: {
766
+ account: Address
767
+ genesisConfigId: Hex.Hex
768
+ init?: MultisigConfig.Config | undefined
769
+ signatures: readonly Hex.Hex[]
770
+ transaction: Record<string, unknown>
771
+ }) {
772
+ const { signature: _, ...transaction } = options.transaction
773
+ const envelope = TxEnvelopeTempo.from({
774
+ ...transaction,
775
+ feePayerSignature: (() => {
776
+ const value = transaction['feePayerSignature']
777
+ // Preserve missing/null fee-payer markers so sponsorship can add signature later.
778
+ if (!value) return value
779
+ // Normalize RPC-shaped signatures before rebuilding final transaction envelope.
780
+ if (typeof value === 'object' && 'r' in value && 's' in value) {
781
+ const signature = value as {
782
+ r: bigint | number | string
783
+ s: bigint | number | string
784
+ v?: bigint | number | string | undefined
785
+ yParity?: bigint | number | string | undefined
786
+ }
787
+ const yParity = (() => {
788
+ const value = signature.yParity ?? signature.v
789
+ // Some signature shapes omit parity until later normalization.
790
+ if (typeof value === 'undefined') return undefined
791
+ const number = Number(value)
792
+ // RPC signatures may carry Ethereum 27/28 `v`; `ox` expects 0/1 parity.
793
+ if (number === 27 || number === 28) return number - 27
794
+ return number
795
+ })()
796
+ if (typeof yParity === 'number')
797
+ return Signature.from({ r: BigInt(signature.r), s: BigInt(signature.s), yParity })
798
+ }
799
+ // Fall back to `ox` for already-normalized signature inputs.
800
+ return Signature.from(value as never)
801
+ })(),
802
+ } as never)
803
+ const payload = TxEnvelopeTempo.getSignPayload(envelope)
804
+ const signatures = options.signatures.map((approval) => SignatureEnvelope.from(approval))
805
+ const sorted = SignatureEnvelope.sortMultisigApprovals({
806
+ account: options.account,
807
+ genesisConfigId: options.genesisConfigId,
808
+ payload,
809
+ signatures,
810
+ })
811
+ const signature = SignatureEnvelope.from({
812
+ account: options.account,
813
+ genesisConfigId: options.genesisConfigId,
814
+ signatures: sorted,
815
+ ...(options.init ? { init: options.init } : {}),
816
+ })
817
+ return TxEnvelopeTempo.serialize(envelope, {
818
+ ...('feePayerSignature' in transaction
819
+ ? { feePayerSignature: transaction['feePayerSignature'] as never }
820
+ : {}),
821
+ signature,
822
+ })
823
+ }
824
+
825
+ function mergeOperation(
826
+ existing: Operation | undefined,
827
+ operation: Operation,
828
+ patch: Partial<Operation>,
829
+ ) {
830
+ return {
831
+ ...operation,
832
+ ...existing,
833
+ ...patch,
834
+ config: operation.config ?? existing?.config,
835
+ init: existing?.init || operation.init,
836
+ initConfig: existing?.initConfig ?? operation.initConfig,
837
+ signatures: mergeSignatures([...(existing?.signatures ?? []), ...operation.signatures]),
838
+ transaction: operation.transaction,
839
+ } satisfies Operation
840
+ }
841
+
842
+ function toStatus(options: {
843
+ approvals: { signatures: readonly Hex.Hex[]; threshold: number; weight: number }
844
+ operation: Operation
845
+ }): Status {
846
+ const { approvals, operation } = options
847
+ return {
848
+ account: operation.account,
849
+ chainId: operation.chainId,
850
+ genesisConfigId: operation.genesisConfigId,
851
+ id: operation.id,
852
+ signatures: approvals.signatures.length,
853
+ status: operation.status,
854
+ submittedHash: operation.submittedHash,
855
+ threshold: approvals.threshold,
856
+ weight: approvals.weight,
857
+ }
858
+ }