accounts 0.6.7 → 0.7.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 (50) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/ExecutionError.d.ts +25 -0
  3. package/dist/core/ExecutionError.d.ts.map +1 -0
  4. package/dist/core/ExecutionError.js +170 -0
  5. package/dist/core/ExecutionError.js.map +1 -0
  6. package/dist/core/Schema.d.ts +33 -7
  7. package/dist/core/Schema.d.ts.map +1 -1
  8. package/dist/core/zod/rpc.d.ts +14 -1
  9. package/dist/core/zod/rpc.d.ts.map +1 -1
  10. package/dist/core/zod/rpc.js +14 -1
  11. package/dist/core/zod/rpc.js.map +1 -1
  12. package/dist/server/CliAuth.d.ts +110 -43
  13. package/dist/server/CliAuth.d.ts.map +1 -1
  14. package/dist/server/CliAuth.js +243 -155
  15. package/dist/server/CliAuth.js.map +1 -1
  16. package/dist/server/Handler.d.ts +0 -1
  17. package/dist/server/Handler.d.ts.map +1 -1
  18. package/dist/server/Handler.js +0 -1
  19. package/dist/server/Handler.js.map +1 -1
  20. package/dist/server/internal/handlers/relay.d.ts +29 -12
  21. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  22. package/dist/server/internal/handlers/relay.js +180 -125
  23. package/dist/server/internal/handlers/relay.js.map +1 -1
  24. package/dist/server/internal/handlers/sponsorship.d.ts +77 -0
  25. package/dist/server/internal/handlers/sponsorship.d.ts.map +1 -0
  26. package/dist/server/internal/handlers/sponsorship.js +96 -0
  27. package/dist/server/internal/handlers/sponsorship.js.map +1 -0
  28. package/dist/server/internal/handlers/utils.d.ts +3 -1
  29. package/dist/server/internal/handlers/utils.d.ts.map +1 -1
  30. package/dist/server/internal/handlers/utils.js +15 -12
  31. package/dist/server/internal/handlers/utils.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/core/ExecutionError.test.ts +205 -0
  34. package/src/core/ExecutionError.ts +189 -0
  35. package/src/core/Provider.test.ts +4 -2
  36. package/src/core/zod/rpc.ts +18 -1
  37. package/src/server/CliAuth.test-d.ts +6 -0
  38. package/src/server/CliAuth.test.ts +83 -0
  39. package/src/server/CliAuth.ts +331 -208
  40. package/src/server/Handler.ts +0 -1
  41. package/src/server/internal/handlers/relay.test.ts +318 -108
  42. package/src/server/internal/handlers/relay.ts +243 -138
  43. package/src/server/internal/handlers/sponsorship.ts +172 -0
  44. package/src/server/internal/handlers/utils.ts +15 -10
  45. package/dist/server/internal/handlers/feePayer.d.ts +0 -73
  46. package/dist/server/internal/handlers/feePayer.d.ts.map +0 -1
  47. package/dist/server/internal/handlers/feePayer.js +0 -184
  48. package/dist/server/internal/handlers/feePayer.js.map +0 -1
  49. package/src/server/internal/handlers/feePayer.test.ts +0 -336
  50. package/src/server/internal/handlers/feePayer.ts +0 -271
@@ -3,7 +3,6 @@ import { Hono } from 'hono'
3
3
  import * as RequestListener from './internal/requestListener.js'
4
4
 
5
5
  export { codeAuth } from './internal/handlers/codeAuth.js'
6
- export { feePayer } from './internal/handlers/feePayer.js'
7
6
  export { relay } from './internal/handlers/relay.js'
8
7
  export { webAuthn } from './internal/handlers/webAuthn.js'
9
8
 
@@ -2,6 +2,7 @@ import type { RpcRequest } from 'ox'
2
2
  import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
3
3
  import { parseUnits } from 'viem'
4
4
  import { fillTransaction, sendTransactionSync } from 'viem/actions'
5
+ import { tempoModerato } from 'viem/chains'
5
6
  import { Actions, Addresses, Capabilities, Tick, Transaction } from 'viem/tempo'
6
7
  import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vp/test'
7
8
 
@@ -31,21 +32,23 @@ const transferCall = () =>
31
32
  amount: 1n,
32
33
  })
33
34
 
34
- describe('behavior: without feePayer', () => {
35
+ beforeAll(async () => {
36
+ // Fund userAccount with alphaUsd for fees + transfers.
37
+ const rpc = getClient()
38
+ await Actions.token.mintSync(rpc, {
39
+ account: accounts[0]!,
40
+ token: addresses.alphaUsd,
41
+ amount: parseUnits('100', 6),
42
+ to: userAccount.address,
43
+ })
44
+ await Actions.fee.setUserToken(rpc, { account: userAccount, token: addresses.alphaUsd })
45
+ })
46
+
47
+ describe('default', () => {
35
48
  let client: ReturnType<typeof getClient<typeof chain>>
36
49
  let server: Server
37
50
 
38
51
  beforeAll(async () => {
39
- // Fund userAccount with alphaUsd for fees + transfers.
40
- const rpc = getClient()
41
- await Actions.token.mintSync(rpc, {
42
- account: accounts[0]!,
43
- token: addresses.alphaUsd,
44
- amount: parseUnits('100', 6),
45
- to: userAccount.address,
46
- })
47
- await Actions.fee.setUserToken(rpc, { account: userAccount, token: addresses.alphaUsd })
48
-
49
52
  server = await createServer(
50
53
  relay({
51
54
  chains: [chain],
@@ -95,6 +98,154 @@ describe('behavior: without feePayer', () => {
95
98
  })
96
99
  })
97
100
 
101
+ describe('behavior: with feePayer', () => {
102
+ let server: Server
103
+ let client: ReturnType<typeof getClient<typeof chain>>
104
+ let requests: RpcRequest.RpcRequest[] = []
105
+
106
+ beforeAll(async () => {
107
+ server = await createServer(
108
+ relay({
109
+ chains: [chain],
110
+ transports: { [chain.id]: http() },
111
+ feePayer: {
112
+ account: feePayerAccount,
113
+ name: 'Test Sponsor',
114
+ url: 'https://test.com',
115
+ },
116
+ onRequest: async (request) => {
117
+ requests.push(request)
118
+ },
119
+ }).listener,
120
+ )
121
+ client = getClient({ transport: http(server.url) })
122
+ })
123
+
124
+ afterAll(() => {
125
+ server.close()
126
+ })
127
+
128
+ afterEach(() => {
129
+ requests = []
130
+ })
131
+
132
+ test('default: returns sponsored tx with feePayerSignature', async () => {
133
+ const { transaction } = await fillTransaction(client, {
134
+ account: userAccount.address,
135
+ calls: [transferCall()],
136
+ })
137
+
138
+ expect(transaction.feePayerSignature).toBeDefined()
139
+ expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
140
+ [
141
+ "eth_fillTransaction",
142
+ ]
143
+ `)
144
+ })
145
+
146
+ test('behavior: returns sponsor capabilities', async () => {
147
+ const result = await fillTransaction(client, {
148
+ account: userAccount.address,
149
+ calls: [transferCall()],
150
+ })
151
+ const meta = result.capabilities
152
+
153
+ expect(meta?.sponsored).toBe(true)
154
+ expect(meta?.sponsor).toMatchInlineSnapshot(`
155
+ {
156
+ "address": "${feePayerAccount.address}",
157
+ "name": "Test Sponsor",
158
+ "url": "https://test.com",
159
+ }
160
+ `)
161
+ })
162
+
163
+ test('behavior: sponsored tx can be signed and broadcast', async () => {
164
+ const { transaction } = await fillTransaction(client, {
165
+ account: userAccount.address,
166
+ calls: [transferCall()],
167
+ })
168
+ const serialized = (await Transaction.serialize(transaction as never)) as `0x76${string}`
169
+ const envelope = TxEnvelopeTempo.deserialize(serialized)
170
+ const signature = await userAccount.sign({
171
+ hash: TxEnvelopeTempo.getSignPayload(envelope),
172
+ })
173
+ const signed = TxEnvelopeTempo.serialize(envelope, {
174
+ signature: SignatureEnvelope.from(signature),
175
+ })
176
+ const receipt = (await getClient().request({
177
+ method: 'eth_sendRawTransactionSync' as never,
178
+ params: [signed],
179
+ })) as { feePayer?: string | undefined }
180
+
181
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
182
+ })
183
+
184
+ test('behavior: missing from returns error capability', async () => {
185
+ const result = await fillTransaction(client, { calls: [transferCall()] })
186
+ expect(result.capabilities).toMatchInlineSnapshot(`
187
+ {
188
+ "error": {
189
+ "errorName": "unknown",
190
+ "message": "unknown account",
191
+ },
192
+ "sponsored": false,
193
+ }
194
+ `)
195
+ })
196
+ })
197
+
198
+ describe('behavior: chainId path parameter', () => {
199
+ let server: Server
200
+ let client: ReturnType<typeof getClient<typeof chain>>
201
+
202
+ beforeAll(async () => {
203
+ server = await createServer(
204
+ relay({
205
+ chains: [chain],
206
+ transports: { [chain.id]: http() },
207
+ }).listener,
208
+ )
209
+ client = getClient({ transport: http(`${server.url}/${chain.id}`) })
210
+ })
211
+
212
+ afterAll(() => {
213
+ server.close()
214
+ })
215
+
216
+ test('default: proxies RPC methods via /:chainId path', async () => {
217
+ const chainId = await client.request({ method: 'eth_chainId' })
218
+ expect(Number(chainId)).toMatchInlineSnapshot(`${chain.id}`)
219
+ })
220
+
221
+ test('behavior: fills transaction via /:chainId path', async () => {
222
+ const { transaction } = await fillTransaction(client, {
223
+ account: userAccount.address,
224
+ calls: [transferCall()],
225
+ })
226
+
227
+ expect(transaction.gas).toBeDefined()
228
+ expect(transaction.nonce).toBeDefined()
229
+ })
230
+
231
+ test('behavior: handles batch requests via /:chainId path', async () => {
232
+ const response = await fetch(`${server.url}/${chain.id}`, {
233
+ method: 'POST',
234
+ headers: { 'Content-Type': 'application/json' },
235
+ body: JSON.stringify([
236
+ { jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] },
237
+ { jsonrpc: '2.0', id: 2, method: 'eth_chainId', params: [] },
238
+ ]),
239
+ })
240
+
241
+ expect(response.status).toBe(200)
242
+ const body = (await response.json()) as { id: number; result: string }[]
243
+ expect(body).toHaveLength(2)
244
+ expect(Number(body[0]!.result)).toBe(chain.id)
245
+ expect(Number(body[1]!.result)).toBe(chain.id)
246
+ })
247
+ })
248
+
98
249
  describe('behavior: capabilities', () => {
99
250
  let server: Server
100
251
  let client: ReturnType<typeof getClient<typeof chain>>
@@ -103,6 +254,7 @@ describe('behavior: capabilities', () => {
103
254
  server = await createServer(
104
255
  relay({
105
256
  chains: [chain],
257
+ features: 'all',
106
258
  transports: { [chain.id]: http() },
107
259
  }).listener,
108
260
  )
@@ -333,6 +485,7 @@ describe('behavior: AMM resolution', () => {
333
485
  server = await createServer(
334
486
  relay({
335
487
  chains: [chain],
488
+ features: 'all',
336
489
  transports: { [chain.id]: http() },
337
490
  }).listener,
338
491
  )
@@ -494,6 +647,7 @@ describe('behavior: AMM resolution', () => {
494
647
  const customServer = await createServer(
495
648
  relay({
496
649
  chains: [chain],
650
+ features: 'all',
497
651
  transports: { [chain.id]: http() },
498
652
  autoSwap: { slippage: 0.02 },
499
653
  }).listener,
@@ -563,112 +717,49 @@ describe('behavior: AMM resolution', () => {
563
717
  const customServer = await createServer(
564
718
  relay({
565
719
  chains: [chain],
720
+ features: 'all',
566
721
  transports: { [chain.id]: http() },
567
722
  autoSwap: false,
568
723
  }).listener,
569
724
  )
570
725
  const customClient = getClient({ transport: http(customServer.url) })
571
726
 
572
- // Should throw InsufficientBalance instead of auto-swapping.
573
- await expect(
574
- fillTransaction(customClient, {
575
- account: sender.address,
576
- ...Actions.token.transfer.call({
577
- token: base,
578
- to: accounts[7]!.address,
579
- amount: parseUnits('5', 6),
580
- }),
727
+ // Should return error capability instead of auto-swapping.
728
+ const result = await fillTransaction(customClient, {
729
+ account: sender.address,
730
+ ...Actions.token.transfer.call({
731
+ token: base,
732
+ to: accounts[7]!.address,
733
+ amount: parseUnits('5', 6),
581
734
  }),
582
- ).rejects.toThrow()
583
- customServer.close()
584
- })
585
- })
586
-
587
- describe('behavior: with feePayer', () => {
588
- let server: Server
589
- let client: ReturnType<typeof getClient<typeof chain>>
590
- let requests: RpcRequest.RpcRequest[] = []
591
-
592
- beforeAll(async () => {
593
- server = await createServer(
594
- relay({
595
- chains: [chain],
596
- transports: { [chain.id]: http() },
597
- feePayer: {
598
- account: feePayerAccount,
599
- name: 'Test Sponsor',
600
- url: 'https://test.com',
601
- },
602
- onRequest: async (request) => {
603
- requests.push(request)
604
- },
605
- }).listener,
606
- )
607
- client = getClient({ transport: http(server.url) })
608
- })
609
-
610
- afterAll(() => {
611
- server.close()
612
- })
613
-
614
- afterEach(() => {
615
- requests = []
616
- })
617
-
618
- test('default: returns sponsored tx with feePayerSignature', async () => {
619
- const { transaction } = await fillTransaction(client, {
620
- account: userAccount.address,
621
- calls: [transferCall()],
622
735
  })
623
-
624
- expect(transaction.feePayerSignature).toBeDefined()
625
- expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
626
- [
627
- "eth_fillTransaction",
628
- ]
736
+ const error = result.capabilities?.error
737
+ expect({ ...error, data: undefined }).toMatchInlineSnapshot(`
738
+ {
739
+ "abiItem": {
740
+ "inputs": [
741
+ {
742
+ "name": "available",
743
+ "type": "uint256",
744
+ },
745
+ {
746
+ "name": "required",
747
+ "type": "uint256",
748
+ },
749
+ {
750
+ "name": "token",
751
+ "type": "address",
752
+ },
753
+ ],
754
+ "name": "InsufficientBalance",
755
+ "type": "error",
756
+ },
757
+ "data": undefined,
758
+ "errorName": "InsufficientBalance",
759
+ "message": "Insufficient balance. Required: 5000000, available: 0.",
760
+ }
629
761
  `)
630
- })
631
-
632
- test('behavior: returns sponsor capabilities', async () => {
633
- const result = await fillTransaction(client, {
634
- account: userAccount.address,
635
- calls: [transferCall()],
636
- })
637
- const meta = result.capabilities
638
-
639
- expect(meta?.sponsored).toBe(true)
640
- expect(meta?.sponsor).toMatchInlineSnapshot(`
641
- {
642
- "address": "${feePayerAccount.address}",
643
- "name": "Test Sponsor",
644
- "url": "https://test.com",
645
- }
646
- `)
647
- })
648
-
649
- test('behavior: sponsored tx can be signed and broadcast', async () => {
650
- const { transaction } = await fillTransaction(client, {
651
- account: userAccount.address,
652
- calls: [transferCall()],
653
- })
654
- const serialized = (await Transaction.serialize(transaction as never)) as `0x76${string}`
655
- const envelope = TxEnvelopeTempo.deserialize(serialized)
656
- const signature = await userAccount.sign({
657
- hash: TxEnvelopeTempo.getSignPayload(envelope),
658
- })
659
- const signed = TxEnvelopeTempo.serialize(envelope, {
660
- signature: SignatureEnvelope.from(signature),
661
- })
662
- const receipt = (await getClient().request({
663
- method: 'eth_sendRawTransactionSync' as never,
664
- params: [signed],
665
- })) as { feePayer?: string | undefined }
666
-
667
- expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
668
- })
669
-
670
- test('behavior: missing from returns error', async () => {
671
- await expect(fillTransaction(client, { calls: [transferCall()] })).rejects.toThrowError()
762
+ customServer.close()
672
763
  })
673
764
  })
674
765
 
@@ -781,6 +872,7 @@ describe('behavior: fee token resolution', () => {
781
872
  server = await createServer(
782
873
  relay({
783
874
  chains: [chain],
875
+ features: 'all',
784
876
  transports: { [chain.id]: http() },
785
877
  }).listener,
786
878
  )
@@ -854,3 +946,121 @@ describe('behavior: fee token resolution', () => {
854
946
  expect(transaction.feeToken).toBeUndefined()
855
947
  })
856
948
  })
949
+
950
+ describe('behavior: error capabilities', () => {
951
+ let server: Server
952
+ let client: ReturnType<typeof getClient<typeof chain>>
953
+
954
+ beforeAll(async () => {
955
+ server = await createServer(
956
+ relay({
957
+ chains: [tempoModerato],
958
+ features: 'all',
959
+ transports: { [tempoModerato.id]: http('https://rpc.moderato.tempo.xyz') },
960
+ }).listener,
961
+ )
962
+ client = getClient({ chain: tempoModerato, transport: http(server.url) })
963
+ })
964
+
965
+ afterAll(() => {
966
+ server.close()
967
+ })
968
+
969
+ test('behavior: returns requireFunds on InsufficientBalance', async () => {
970
+ const sender = accounts[10]!
971
+
972
+ const result = await fillTransaction(client, {
973
+ account: sender.address,
974
+ calls: [
975
+ Actions.token.transfer.call({
976
+ token: addresses.alphaUsd,
977
+ to: recipient.address,
978
+ amount: parseUnits('100', 6),
979
+ }),
980
+ ],
981
+ })
982
+
983
+ expect(result.capabilities).toMatchInlineSnapshot(`
984
+ {
985
+ "balanceDiffs": {
986
+ "0x0eB552e73e6f8E0922749e0fB08af2a71ECb2b7F": [
987
+ {
988
+ "address": "0x20c0000000000000000000000000000000000001",
989
+ "decimals": 6,
990
+ "direction": "outgoing",
991
+ "formatted": "100",
992
+ "name": "AlphaUSD",
993
+ "recipients": [
994
+ "0xAF4311d557fBC876059e39306ec1f3343753df29",
995
+ ],
996
+ "symbol": "AlphaUSD",
997
+ "value": "0x5f5e100",
998
+ },
999
+ ],
1000
+ },
1001
+ "error": {
1002
+ "abiItem": {
1003
+ "inputs": [
1004
+ {
1005
+ "name": "available",
1006
+ "type": "uint256",
1007
+ },
1008
+ {
1009
+ "name": "required",
1010
+ "type": "uint256",
1011
+ },
1012
+ {
1013
+ "name": "token",
1014
+ "type": "address",
1015
+ },
1016
+ ],
1017
+ "name": "InsufficientBalance",
1018
+ "type": "error",
1019
+ },
1020
+ "data": "0x832f98b500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000020c0000000000000000000000000000000000001",
1021
+ "errorName": "InsufficientBalance",
1022
+ "message": "Insufficient balance. Required: 100000000, available: 0.",
1023
+ },
1024
+ "requireFunds": {
1025
+ "amount": "0x5f5e100",
1026
+ "decimals": 6,
1027
+ "formatted": "100",
1028
+ "symbol": "AlphaUSD",
1029
+ "token": "0x20C0000000000000000000000000000000000001",
1030
+ },
1031
+ "sponsored": false,
1032
+ }
1033
+ `)
1034
+ })
1035
+
1036
+ test('behavior: returns error capability on generic revert', async () => {
1037
+ const sender = accounts[10]!
1038
+
1039
+ const result = await fillTransaction(client, {
1040
+ account: sender.address,
1041
+ calls: [
1042
+ Actions.token.grantRoles.call({
1043
+ token: addresses.alphaUsd,
1044
+ role: 'issuer',
1045
+ to: sender.address,
1046
+ }),
1047
+ ],
1048
+ })
1049
+
1050
+ expect(result.capabilities).toMatchInlineSnapshot(`
1051
+ {
1052
+ "error": {
1053
+ "abiItem": {
1054
+ "inputs": [],
1055
+ "name": "Unauthorized",
1056
+ "type": "error",
1057
+ },
1058
+ "data": "0x82b42900",
1059
+ "errorName": "Unauthorized",
1060
+ "message": "Unauthorized.",
1061
+ },
1062
+ "sponsored": false,
1063
+ }
1064
+ `)
1065
+ })
1066
+ })