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,744 @@
1
+ import { Address, Hex, P256 } from 'ox'
2
+ import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
3
+ import { createClient, custom } from 'viem'
4
+ import { Account, Transaction } from 'viem/tempo'
5
+ import { tempoDevnet } from 'viem/tempo/chains'
6
+
7
+ import * as Multisig from './multisig.js'
8
+
9
+ const privateKey_1 = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
10
+ const privateKey_2 = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'
11
+ const privateKey_3 = '0x7c852118294f7e395961158d39c448445a22a434adcd1d4e9048bcd3d85e9e4d'
12
+ const feeToken = '0x20c0000000000000000000000000000000000000' as const
13
+
14
+ describe('memoryStore', () => {
15
+ test('behavior: submission claims are exclusive until TTL expiry', async () => {
16
+ const store = Multisig.memoryStore()
17
+ const operation = createOperation()
18
+
19
+ await store.savePending(operation)
20
+ const first = await store.claimSubmission(operation, { ttl: 1_000 })
21
+ const second = await store.claimSubmission(operation, { ttl: 1_000 })
22
+
23
+ expect({ first: first.status, second: second.status }).toMatchInlineSnapshot(`
24
+ {
25
+ "first": "claimed",
26
+ "second": "submitting",
27
+ }
28
+ `)
29
+ })
30
+
31
+ test('behavior: expired submission claims can be reclaimed', async () => {
32
+ const store = Multisig.memoryStore()
33
+ const operation = createOperation()
34
+
35
+ await store.savePending(operation)
36
+ await store.claimSubmission(operation, { ttl: -1 })
37
+ const reclaimed = await store.claimSubmission(operation, { ttl: 1_000 })
38
+
39
+ expect(reclaimed.status).toMatchInlineSnapshot(`"claimed"`)
40
+ })
41
+
42
+ test('behavior: pending saves merge signatures and submitted operations are not listed as pending', async () => {
43
+ const store = Multisig.memoryStore()
44
+ const operation = createOperation()
45
+
46
+ await store.savePending({ ...operation, signatures: [`0x${'11'.repeat(65)}`] })
47
+ const merged = await store.savePending({ ...operation, signatures: [`0x${'22'.repeat(65)}`] })
48
+ await store.claimSubmission(merged, { ttl: 1_000 })
49
+ await store.setSubmitted(operation.id, `0x${'33'.repeat(32)}`)
50
+
51
+ expect({
52
+ listed: await store.listPendingByAddress(operation.account),
53
+ signatures: merged.signatures.length,
54
+ status: (await store.get(operation.id))?.status,
55
+ }).toMatchInlineSnapshot(`
56
+ {
57
+ "listed": [],
58
+ "signatures": 2,
59
+ "status": "submitted",
60
+ }
61
+ `)
62
+ })
63
+ })
64
+
65
+ describe('handleRawTransaction', () => {
66
+ test('behavior: rejects first approvals when no config can be resolved', async () => {
67
+ const owner = Account.fromSecp256k1(privateKey_1)
68
+ const account = Account.fromMultisig({
69
+ threshold: 1,
70
+ owners: [{ owner: owner.address, weight: 1 }],
71
+ })
72
+ const transaction = {
73
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
74
+ chainId: tempoDevnet.id,
75
+ gas: 21_000n,
76
+ maxFeePerGas: 1n,
77
+ maxPriorityFeePerGas: 0n,
78
+ multisig: account.config,
79
+ nonce: 0n,
80
+ }
81
+ const signature = await owner.signTransaction(transaction as never)
82
+ const serialized = withoutInit(
83
+ await account.signTransaction({ ...transaction, signatures: [signature] } as never),
84
+ )
85
+
86
+ await expect(
87
+ Multisig.handleRawTransaction({
88
+ getClient: (() => undefined) as never,
89
+ method: 'eth_sendRawTransaction',
90
+ request: { params: [serialized] },
91
+ store: Multisig.memoryStore(),
92
+ }),
93
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
94
+ `[RpcResponse.InvalidParamsError: Multisig config is required to collect approvals. Provide it in the bootstrap transaction or configure \`multisig.resolveConfig\`.]`,
95
+ )
96
+ })
97
+
98
+ test('behavior: finalize submitted preserves async broadcast method', async () => {
99
+ const owner = Account.fromSecp256k1(privateKey_1)
100
+ const account = Account.fromMultisig({
101
+ threshold: 1,
102
+ owners: [{ owner: owner.address, weight: 1 }],
103
+ })
104
+ const transaction = {
105
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
106
+ chainId: tempoDevnet.id,
107
+ gas: 21_000n,
108
+ maxFeePerGas: 1n,
109
+ maxPriorityFeePerGas: 0n,
110
+ multisig: account.config,
111
+ nonce: 0n,
112
+ }
113
+ const signature = await owner.signTransaction(transaction as never)
114
+ const serialized = await account.signTransaction({
115
+ ...transaction,
116
+ signatures: [signature],
117
+ } as never)
118
+ let broadcastMethod: string | undefined
119
+
120
+ const hash = await Multisig.handleRawTransaction({
121
+ finalize: 'submitted',
122
+ getClient: (() => ({
123
+ request: async ({ method, params }: { method: string; params: unknown }) => {
124
+ broadcastMethod = method
125
+ return hashRawTransaction(params)
126
+ },
127
+ })) as never,
128
+ method: 'eth_sendRawTransaction',
129
+ request: { params: [serialized] },
130
+ store: Multisig.memoryStore(),
131
+ })
132
+
133
+ expect({ broadcastMethod, hash }).toMatchInlineSnapshot(`
134
+ {
135
+ "broadcastMethod": "eth_sendRawTransaction",
136
+ "hash": "${hash}",
137
+ }
138
+ `)
139
+ })
140
+ })
141
+
142
+ describe('config resolution', () => {
143
+ test('behavior: reuses pending bootstrap config when a later approval omits init', async () => {
144
+ const store = Multisig.memoryStore()
145
+ const owner_1 = Account.fromSecp256k1(privateKey_1)
146
+ const owner_2 = Account.fromSecp256k1(privateKey_2)
147
+ const account = Account.fromMultisig({
148
+ threshold: 2,
149
+ owners: [
150
+ { owner: owner_1.address, weight: 1 },
151
+ { owner: owner_2.address, weight: 1 },
152
+ ],
153
+ })
154
+ const transaction = {
155
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
156
+ chainId: tempoDevnet.id,
157
+ gas: 21_000n,
158
+ maxFeePerGas: 1n,
159
+ maxPriorityFeePerGas: 0n,
160
+ multisig: account.config,
161
+ nonce: 0n,
162
+ }
163
+ const signature_1 = await owner_1.signTransaction(transaction as never)
164
+ const signature_2 = await owner_2.signTransaction(transaction as never)
165
+ const first = await account.signTransaction({
166
+ ...transaction,
167
+ signatures: [signature_1],
168
+ } as never)
169
+ const second = withoutInit(
170
+ await account.signTransaction({ ...transaction, signatures: [signature_2] } as never),
171
+ )
172
+
173
+ await Multisig.handleRawTransaction({
174
+ getClient: (() => undefined) as never,
175
+ method: 'eth_sendRawTransaction',
176
+ request: { params: [first] },
177
+ store,
178
+ })
179
+ const hash = await Multisig.handleRawTransaction({
180
+ getClient: (() => ({
181
+ request: async ({ params }: { params: unknown }) => hashRawTransaction(params),
182
+ })) as never,
183
+ method: 'eth_sendRawTransaction',
184
+ request: { params: [second] },
185
+ store,
186
+ })
187
+
188
+ expect(hash).toMatchInlineSnapshot(`"${hash}"`)
189
+ })
190
+
191
+ test('behavior: preserves bootstrap init when it arrives after a resolver-backed pending approval', async () => {
192
+ const store = Multisig.memoryStore()
193
+ const owner_1 = Account.fromSecp256k1(privateKey_1)
194
+ const owner_2 = Account.fromSecp256k1(privateKey_2)
195
+ const account = Account.fromMultisig({
196
+ threshold: 2,
197
+ owners: [
198
+ { owner: owner_1.address, weight: 1 },
199
+ { owner: owner_2.address, weight: 1 },
200
+ ],
201
+ })
202
+ const transaction = {
203
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
204
+ chainId: tempoDevnet.id,
205
+ gas: 21_000n,
206
+ maxFeePerGas: 1n,
207
+ maxPriorityFeePerGas: 0n,
208
+ multisig: account.config,
209
+ nonce: 0n,
210
+ }
211
+ const signature_1 = await owner_1.signTransaction(transaction as never)
212
+ const signature_2 = await owner_2.signTransaction(transaction as never)
213
+ const first = withoutInit(
214
+ await account.signTransaction({ ...transaction, signatures: [signature_1] } as never),
215
+ )
216
+ const second = await account.signTransaction({
217
+ ...transaction,
218
+ signatures: [signature_2],
219
+ } as never)
220
+ let submitted: `0x${string}` | undefined
221
+
222
+ await Multisig.handleRawTransaction({
223
+ getClient: (() => undefined) as never,
224
+ method: 'eth_sendRawTransaction',
225
+ request: { params: [first] },
226
+ resolveConfig: () => account.config,
227
+ store,
228
+ })
229
+ const hash = await Multisig.handleRawTransaction({
230
+ getClient: (() => ({
231
+ request: async ({ params }: { params: unknown }) => {
232
+ submitted = Array.isArray(params) ? (params[0] as `0x${string}`) : undefined
233
+ return hashRawTransaction(params)
234
+ },
235
+ })) as never,
236
+ method: 'eth_sendRawTransaction',
237
+ request: { params: [second] },
238
+ resolveConfig: () => account.config,
239
+ store,
240
+ })
241
+
242
+ const transaction_submitted = Transaction.deserialize(submitted! as never) as {
243
+ signature: SignatureEnvelope.Multisig
244
+ }
245
+ expect({
246
+ hash,
247
+ init: transaction_submitted.signature.init
248
+ ? MultisigConfig.toId(transaction_submitted.signature.init)
249
+ : undefined,
250
+ }).toMatchInlineSnapshot(`
251
+ {
252
+ "hash": "${hash}",
253
+ "init": "${MultisigConfig.toId(account.config)}",
254
+ }
255
+ `)
256
+ })
257
+
258
+ test('behavior: attaches resolver-provided bootstrap config when approvals omit init', async () => {
259
+ const store = Multisig.memoryStore()
260
+ const owner_1 = Account.fromSecp256k1(privateKey_1)
261
+ const owner_2 = Account.fromSecp256k1(privateKey_2)
262
+ const account = Account.fromMultisig({
263
+ threshold: 2,
264
+ owners: [
265
+ { owner: owner_1.address, weight: 1 },
266
+ { owner: owner_2.address, weight: 1 },
267
+ ],
268
+ })
269
+ const transaction = {
270
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
271
+ chainId: tempoDevnet.id,
272
+ gas: 21_000n,
273
+ maxFeePerGas: 1n,
274
+ maxPriorityFeePerGas: 0n,
275
+ multisig: account.config,
276
+ nonce: 0n,
277
+ }
278
+ const signature_1 = await owner_1.signTransaction(transaction as never)
279
+ const signature_2 = await owner_2.signTransaction(transaction as never)
280
+ const first = withoutInit(
281
+ await account.signTransaction({ ...transaction, signatures: [signature_1] } as never),
282
+ )
283
+ const second = withoutInit(
284
+ await account.signTransaction({ ...transaction, signatures: [signature_2] } as never),
285
+ )
286
+ let submitted: `0x${string}` | undefined
287
+
288
+ await Multisig.handleRawTransaction({
289
+ getClient: (() => undefined) as never,
290
+ method: 'eth_sendRawTransaction',
291
+ request: { params: [first] },
292
+ resolveConfig: () => account.config,
293
+ store,
294
+ })
295
+ const hash = await Multisig.handleRawTransaction({
296
+ getClient: (() => ({
297
+ request: async ({ params }: { params: unknown }) => {
298
+ submitted = Array.isArray(params) ? (params[0] as `0x${string}`) : undefined
299
+ return hashRawTransaction(params)
300
+ },
301
+ })) as never,
302
+ method: 'eth_sendRawTransaction',
303
+ request: { params: [second] },
304
+ resolveConfig: () => account.config,
305
+ store,
306
+ })
307
+
308
+ const transaction_submitted = Transaction.deserialize(submitted! as never) as {
309
+ signature: SignatureEnvelope.Multisig
310
+ }
311
+ expect({
312
+ hash,
313
+ init: transaction_submitted.signature.init
314
+ ? MultisigConfig.toId(transaction_submitted.signature.init)
315
+ : undefined,
316
+ }).toMatchInlineSnapshot(`
317
+ {
318
+ "hash": "${hash}",
319
+ "init": "${MultisigConfig.toId(account.config)}",
320
+ }
321
+ `)
322
+ })
323
+
324
+ test('behavior: preserves bootstrap init when claimed operation omits stored init config', async () => {
325
+ const base = Multisig.memoryStore()
326
+ const store: Multisig.Store = {
327
+ claimSubmission: async (operation, options) => {
328
+ const result = await base.claimSubmission(operation, options)
329
+ if (result.status !== 'claimed') return result
330
+ return {
331
+ operation: { ...result.operation, initConfig: undefined },
332
+ status: result.status,
333
+ }
334
+ },
335
+ get: base.get,
336
+ listPendingByAddress: base.listPendingByAddress,
337
+ savePending: base.savePending,
338
+ setSubmitted: base.setSubmitted,
339
+ }
340
+ const owner = Account.fromSecp256k1(privateKey_1)
341
+ const account = Account.fromMultisig({
342
+ threshold: 1,
343
+ owners: [{ owner: owner.address, weight: 1 }],
344
+ })
345
+ const transaction = {
346
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
347
+ chainId: tempoDevnet.id,
348
+ gas: 21_000n,
349
+ maxFeePerGas: 1n,
350
+ maxPriorityFeePerGas: 0n,
351
+ multisig: account.config,
352
+ nonce: 0n,
353
+ }
354
+ const signature = await owner.signTransaction(transaction as never)
355
+ const serialized = await account.signTransaction({
356
+ ...transaction,
357
+ signatures: [signature],
358
+ } as never)
359
+ let submitted: `0x${string}` | undefined
360
+
361
+ const hash = await Multisig.handleRawTransaction({
362
+ getClient: (() => ({
363
+ request: async ({ params }: { params: unknown }) => {
364
+ submitted = Array.isArray(params) ? (params[0] as `0x${string}`) : undefined
365
+ return hashRawTransaction(params)
366
+ },
367
+ })) as never,
368
+ method: 'eth_sendRawTransaction',
369
+ request: { params: [serialized] },
370
+ store,
371
+ })
372
+
373
+ const transaction_submitted = Transaction.deserialize(submitted! as never) as {
374
+ signature: SignatureEnvelope.Multisig
375
+ }
376
+ expect({
377
+ hash,
378
+ init: transaction_submitted.signature.init
379
+ ? MultisigConfig.toId(transaction_submitted.signature.init)
380
+ : undefined,
381
+ }).toMatchInlineSnapshot(`
382
+ {
383
+ "hash": "${hash}",
384
+ "init": "${MultisigConfig.toId(account.config)}",
385
+ }
386
+ `)
387
+ })
388
+
389
+ test('behavior: rejects resolved configs that do not match the multisig account', async () => {
390
+ const owner_1 = Account.fromSecp256k1(privateKey_1)
391
+ const owner_2 = Account.fromSecp256k1(privateKey_2)
392
+ const account = Account.fromMultisig({
393
+ threshold: 1,
394
+ owners: [{ owner: owner_1.address, weight: 1 }],
395
+ })
396
+ const wrong = Account.fromMultisig({
397
+ threshold: 1,
398
+ owners: [{ owner: owner_2.address, weight: 1 }],
399
+ })
400
+ const transaction = {
401
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
402
+ chainId: tempoDevnet.id,
403
+ gas: 21_000n,
404
+ maxFeePerGas: 1n,
405
+ maxPriorityFeePerGas: 0n,
406
+ multisig: account.config,
407
+ nonce: 0n,
408
+ }
409
+ const signature = await owner_1.signTransaction(transaction as never)
410
+ const serialized = await account.signTransaction({
411
+ ...transaction,
412
+ signatures: [signature],
413
+ } as never)
414
+
415
+ await expect(
416
+ Multisig.handleRawTransaction({
417
+ getClient: (() => undefined) as never,
418
+ method: 'eth_sendRawTransaction',
419
+ request: { params: [serialized] },
420
+ resolveConfig: () => wrong.config,
421
+ store: Multisig.memoryStore(),
422
+ }),
423
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
424
+ `[RpcResponse.InvalidParamsError: Signature from non-owner 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266.]`,
425
+ )
426
+ })
427
+
428
+ test('behavior: accepts current configs for updated multisig signer sets', async () => {
429
+ const owner_1 = Account.fromSecp256k1(privateKey_1)
430
+ const owner_2 = Account.fromSecp256k1(privateKey_2)
431
+ const account = Account.fromMultisig({
432
+ threshold: 1,
433
+ owners: [{ owner: owner_1.address, weight: 1 }],
434
+ })
435
+ const current = MultisigConfig.from({
436
+ owners: [
437
+ { owner: owner_1.address, weight: 1 },
438
+ { owner: owner_2.address, weight: 1 },
439
+ ],
440
+ threshold: 1,
441
+ })
442
+ const transaction = {
443
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
444
+ chainId: tempoDevnet.id,
445
+ gas: 21_000n,
446
+ maxFeePerGas: 1n,
447
+ maxPriorityFeePerGas: 0n,
448
+ multisig: account.config,
449
+ nonce: 1n,
450
+ }
451
+ const signature = await owner_2.signTransaction(transaction as never)
452
+ const serialized = withoutInit(
453
+ await account.signTransaction({
454
+ ...transaction,
455
+ signatures: [signature],
456
+ } as never),
457
+ )
458
+ let submitted: `0x${string}` | undefined
459
+
460
+ const hash = await Multisig.handleRawTransaction({
461
+ getClient: (() => ({
462
+ request: async ({ params }: { params: unknown }) => {
463
+ submitted = Array.isArray(params) ? (params[0] as `0x${string}`) : undefined
464
+ return hashRawTransaction(params)
465
+ },
466
+ })) as never,
467
+ method: 'eth_sendRawTransaction',
468
+ request: { params: [serialized] },
469
+ resolveConfig: () => current,
470
+ store: Multisig.memoryStore(),
471
+ })
472
+
473
+ const transaction_submitted = Transaction.deserialize(submitted! as never) as {
474
+ signature: SignatureEnvelope.Multisig
475
+ }
476
+ expect({
477
+ hash,
478
+ init: transaction_submitted.signature.init,
479
+ genesisConfigId: transaction_submitted.signature.genesisConfigId,
480
+ }).toMatchInlineSnapshot(`
481
+ {
482
+ "genesisConfigId": "${MultisigConfig.toId(account.config)}",
483
+ "hash": "${hash}",
484
+ "init": undefined,
485
+ }
486
+ `)
487
+ })
488
+ })
489
+
490
+ describe('approval validation', () => {
491
+ test('behavior: rejects P256 approvals whose embedded owner key did not sign the operation', async () => {
492
+ const privateKey = P256.randomPrivateKey()
493
+ const publicKey = P256.getPublicKey({ privateKey })
494
+ const owner = Address.fromPublicKey(publicKey)
495
+ const account = Account.fromMultisig({
496
+ threshold: 1,
497
+ owners: [{ owner, weight: 1 }],
498
+ })
499
+ const transaction = {
500
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
501
+ chainId: tempoDevnet.id,
502
+ gas: 21_000n,
503
+ maxFeePerGas: 1n,
504
+ maxPriorityFeePerGas: 0n,
505
+ multisig: account.config,
506
+ nonce: 0n,
507
+ }
508
+ const signature = SignatureEnvelope.serialize(
509
+ SignatureEnvelope.from({
510
+ publicKey,
511
+ signature: P256.sign({ payload: `0x${'44'.repeat(32)}`, privateKey }),
512
+ type: 'p256',
513
+ }),
514
+ )
515
+ const serialized = await account.signTransaction({
516
+ ...transaction,
517
+ signatures: [signature],
518
+ } as never)
519
+
520
+ await expect(
521
+ Multisig.handleRawTransaction({
522
+ getClient: (() => ({
523
+ request: async ({ params }: { params: unknown }) => hashRawTransaction(params),
524
+ })) as never,
525
+ method: 'eth_sendRawTransaction',
526
+ request: { params: [serialized] },
527
+ store: Multisig.memoryStore(),
528
+ }),
529
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
530
+ `[RpcResponse.InvalidParamsError: Invalid signature from owner ${owner}.]`,
531
+ )
532
+ })
533
+ })
534
+
535
+ describe('handleRawTransaction with sponsor', () => {
536
+ test('behavior: broadcasts finalized multisig transactions with feePayerSignature and default feeToken', async () => {
537
+ const store = Multisig.memoryStore()
538
+ const owner_1 = Account.fromSecp256k1(privateKey_1)
539
+ const owner_2 = Account.fromSecp256k1(privateKey_2)
540
+ const feePayer = Account.fromSecp256k1(privateKey_3)
541
+ const account = Account.fromMultisig({
542
+ threshold: 2,
543
+ owners: [
544
+ { owner: owner_1.address, weight: 1 },
545
+ { owner: owner_2.address, weight: 1 },
546
+ ],
547
+ })
548
+ const transaction = {
549
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
550
+ chainId: tempoDevnet.id,
551
+ feePayerSignature: null,
552
+ gas: 21_000n,
553
+ maxFeePerGas: 1n,
554
+ maxPriorityFeePerGas: 0n,
555
+ multisig: account.config,
556
+ nonce: 0n,
557
+ }
558
+ const signature_1 = await owner_1.signTransaction(transaction as never)
559
+ const signature_2 = await owner_2.signTransaction(transaction as never)
560
+ const first = await account.signTransaction({
561
+ ...transaction,
562
+ signatures: [signature_1],
563
+ } as never)
564
+ const second = await account.signTransaction({
565
+ ...transaction,
566
+ signatures: [signature_2],
567
+ } as never)
568
+ const broadcasted: string[] = []
569
+
570
+ await Multisig.handleRawTransaction({
571
+ getClient: (() => undefined) as never,
572
+ method: 'eth_sendRawTransaction',
573
+ request: { params: [first] },
574
+ sponsor: {
575
+ account: feePayer as never,
576
+ resolveFeeToken: () => feeToken,
577
+ },
578
+ store,
579
+ })
580
+ const hash = await Multisig.handleRawTransaction({
581
+ getClient: () =>
582
+ createClient({
583
+ chain: tempoDevnet,
584
+ transport: custom({
585
+ request: async ({ method, params }) => {
586
+ if (method === 'eth_chainId') return Hex.fromNumber(tempoDevnet.id)
587
+ broadcasted.push((params as readonly string[])[0]!)
588
+ return hashRawTransaction(params)
589
+ },
590
+ }),
591
+ }),
592
+ method: 'eth_sendRawTransaction',
593
+ request: { params: [second] },
594
+ sponsor: {
595
+ account: feePayer as never,
596
+ resolveFeeToken: () => feeToken,
597
+ },
598
+ store,
599
+ })
600
+
601
+ const transaction_broadcasted = Transaction.deserialize(broadcasted[0] as never) as Record<
602
+ string,
603
+ unknown
604
+ >
605
+ const signature = transaction_broadcasted['signature'] as { init?: unknown; type?: string }
606
+ expect({
607
+ feePayerSignature: Boolean(transaction_broadcasted['feePayerSignature']),
608
+ feeToken: (transaction_broadcasted['feeToken'] as string).toLowerCase(),
609
+ hash,
610
+ init: Boolean(signature.init),
611
+ signature: signature.type,
612
+ }).toMatchInlineSnapshot(`
613
+ {
614
+ "feePayerSignature": true,
615
+ "feeToken": "0x20c0000000000000000000000000000000000000",
616
+ "hash": "${hash}",
617
+ "init": true,
618
+ "signature": "multisig",
619
+ }
620
+ `)
621
+ })
622
+
623
+ test('behavior: broadcasts already sponsored multisig transactions without replacing fee payer', async () => {
624
+ const owner = Account.fromSecp256k1(privateKey_1)
625
+ const feePayer_original = Account.fromSecp256k1(privateKey_2)
626
+ const feePayer_relay = Account.fromSecp256k1(privateKey_3)
627
+ const account = Account.fromMultisig({
628
+ threshold: 1,
629
+ owners: [{ owner: owner.address, weight: 1 }],
630
+ })
631
+ const transaction = {
632
+ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe', value: 1n }],
633
+ chainId: tempoDevnet.id,
634
+ feePayerSignature: null,
635
+ gas: 21_000n,
636
+ maxFeePerGas: 1n,
637
+ maxPriorityFeePerGas: 0n,
638
+ multisig: account.config,
639
+ nonce: 0n,
640
+ }
641
+ const signature = await owner.signTransaction(transaction as never)
642
+ const serialized = await account.signTransaction({
643
+ ...transaction,
644
+ signatures: [signature],
645
+ } as never)
646
+ const sponsored: `0x${string}`[] = []
647
+
648
+ await Multisig.handleRawTransaction({
649
+ getClient: () =>
650
+ createClient({
651
+ chain: tempoDevnet,
652
+ transport: custom({
653
+ request: async ({ method, params }) => {
654
+ if (method === 'eth_chainId') return Hex.fromNumber(tempoDevnet.id)
655
+ sponsored.push((params as readonly `0x${string}`[])[0]!)
656
+ return hashRawTransaction(params)
657
+ },
658
+ }),
659
+ }),
660
+ method: 'eth_sendRawTransaction',
661
+ request: { params: [serialized] },
662
+ sponsor: {
663
+ account: feePayer_original as never,
664
+ resolveFeeToken: () => feeToken,
665
+ },
666
+ store: Multisig.memoryStore(),
667
+ })
668
+ const presponsored = sponsored[0]
669
+ if (!presponsored) throw new Error('Expected pre-sponsored transaction.')
670
+
671
+ let broadcasted: `0x${string}` | undefined
672
+ const hash = await Multisig.handleRawTransaction({
673
+ getClient: (() => ({
674
+ request: async ({ params }: { params: unknown }) => {
675
+ broadcasted = Array.isArray(params) ? (params[0] as `0x${string}`) : undefined
676
+ return hashRawTransaction(params)
677
+ },
678
+ })) as never,
679
+ method: 'eth_sendRawTransaction',
680
+ request: { params: [presponsored] },
681
+ sponsor: {
682
+ account: feePayer_relay as never,
683
+ feeToken,
684
+ validate: () => false,
685
+ },
686
+ store: Multisig.memoryStore(),
687
+ })
688
+
689
+ const transaction_broadcasted = Transaction.deserialize(broadcasted! as never) as Record<
690
+ string,
691
+ unknown
692
+ >
693
+ expect({
694
+ feePayerSignature: Boolean(transaction_broadcasted['feePayerSignature']),
695
+ feeToken: (transaction_broadcasted['feeToken'] as string).toLowerCase(),
696
+ hash,
697
+ signature: (transaction_broadcasted['signature'] as { type?: string }).type,
698
+ }).toMatchInlineSnapshot(`
699
+ {
700
+ "feePayerSignature": true,
701
+ "feeToken": "0x20c0000000000000000000000000000000000000",
702
+ "hash": "${hash}",
703
+ "signature": "multisig",
704
+ }
705
+ `)
706
+ })
707
+ })
708
+
709
+ function createOperation(): Multisig.Operation {
710
+ const now = Date.now()
711
+ return {
712
+ account: '0x0000000000000000000000000000000000000001',
713
+ chainId: tempoDevnet.id,
714
+ createdAt: now,
715
+ genesisConfigId: `0x${'11'.repeat(32)}`,
716
+ id: `0x${'22'.repeat(32)}`,
717
+ payload: `0x${'33'.repeat(32)}`,
718
+ signatures: [],
719
+ status: 'pending',
720
+ transaction: `0x76${'44'.repeat(32)}`,
721
+ updatedAt: now,
722
+ }
723
+ }
724
+
725
+ function hashRawTransaction(params: unknown) {
726
+ const serialized = Array.isArray(params) ? params[0] : undefined
727
+ if (typeof serialized !== 'string') throw new Error('Expected raw transaction.')
728
+ return TxEnvelopeTempo.hash(
729
+ TxEnvelopeTempo.deserialize(serialized as TxEnvelopeTempo.Serialized) as TxEnvelopeTempo.Signed,
730
+ )
731
+ }
732
+
733
+ function withoutInit(serialized: `0x${string}`) {
734
+ const transaction = Transaction.deserialize(serialized as never) as Record<string, unknown>
735
+ const signature = transaction['signature'] as SignatureEnvelope.Multisig
736
+ const { signature: _, ...unsigned } = transaction
737
+ return TxEnvelopeTempo.serialize(TxEnvelopeTempo.from(unsigned as never), {
738
+ signature: SignatureEnvelope.from({
739
+ account: signature.account,
740
+ genesisConfigId: signature.genesisConfigId,
741
+ signatures: signature.signatures,
742
+ }),
743
+ })
744
+ }