accounts 0.3.0 → 0.4.1
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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/core/AccessKey.d.ts +55 -0
- package/dist/core/AccessKey.d.ts.map +1 -0
- package/dist/core/AccessKey.js +69 -0
- package/dist/core/AccessKey.js.map +1 -0
- package/dist/core/Account.d.ts +91 -0
- package/dist/core/Account.d.ts.map +1 -0
- package/dist/core/Account.js +64 -0
- package/dist/core/Account.js.map +1 -0
- package/dist/core/Adapter.d.ts +187 -0
- package/dist/core/Adapter.d.ts.map +1 -0
- package/dist/core/Adapter.js +7 -0
- package/dist/core/Adapter.js.map +1 -0
- package/dist/core/Ceremony.d.ts +109 -0
- package/dist/core/Ceremony.d.ts.map +1 -0
- package/dist/core/Ceremony.js +104 -0
- package/dist/core/Ceremony.js.map +1 -0
- package/dist/core/Client.d.ts +16 -0
- package/dist/core/Client.d.ts.map +1 -0
- package/dist/core/Client.js +18 -0
- package/dist/core/Client.js.map +1 -0
- package/dist/core/Dialog.d.ts +52 -0
- package/dist/core/Dialog.d.ts.map +1 -0
- package/dist/core/Dialog.js +342 -0
- package/dist/core/Dialog.js.map +1 -0
- package/dist/core/Expiry.d.ts +15 -0
- package/dist/core/Expiry.d.ts.map +1 -0
- package/dist/core/Expiry.js +29 -0
- package/dist/core/Expiry.js.map +1 -0
- package/dist/core/Messenger.d.ts +86 -0
- package/dist/core/Messenger.d.ts.map +1 -0
- package/dist/core/Messenger.js +127 -0
- package/dist/core/Messenger.js.map +1 -0
- package/dist/core/Provider.d.ts +69 -0
- package/dist/core/Provider.d.ts.map +1 -0
- package/dist/core/Provider.js +401 -0
- package/dist/core/Provider.js.map +1 -0
- package/dist/core/Remote.d.ts +114 -0
- package/dist/core/Remote.d.ts.map +1 -0
- package/dist/core/Remote.js +116 -0
- package/dist/core/Remote.js.map +1 -0
- package/dist/core/Schema.d.ts +805 -0
- package/dist/core/Schema.d.ts.map +1 -0
- package/dist/core/Schema.js +43 -0
- package/dist/core/Schema.js.map +1 -0
- package/dist/core/Storage.d.ts +42 -0
- package/dist/core/Storage.d.ts.map +1 -0
- package/dist/core/Storage.js +173 -0
- package/dist/core/Storage.js.map +1 -0
- package/dist/core/Store.d.ts +58 -0
- package/dist/core/Store.d.ts.map +1 -0
- package/dist/core/Store.js +58 -0
- package/dist/core/Store.js.map +1 -0
- package/dist/core/adapters/dangerous_secp256k1.d.ts +30 -0
- package/dist/core/adapters/dangerous_secp256k1.d.ts.map +1 -0
- package/dist/core/adapters/dangerous_secp256k1.js +39 -0
- package/dist/core/adapters/dangerous_secp256k1.js.map +1 -0
- package/dist/core/adapters/dialog.d.ts +31 -0
- package/dist/core/adapters/dialog.d.ts.map +1 -0
- package/dist/core/adapters/dialog.js +306 -0
- package/dist/core/adapters/dialog.js.map +1 -0
- package/dist/core/adapters/local.d.ts +33 -0
- package/dist/core/adapters/local.d.ts.map +1 -0
- package/dist/core/adapters/local.js +227 -0
- package/dist/core/adapters/local.js.map +1 -0
- package/dist/core/adapters/webAuthn.d.ts +36 -0
- package/dist/core/adapters/webAuthn.d.ts.map +1 -0
- package/dist/core/adapters/webAuthn.js +93 -0
- package/dist/core/adapters/webAuthn.js.map +1 -0
- package/dist/core/internal/withDedupe.d.ts +12 -0
- package/dist/core/internal/withDedupe.d.ts.map +1 -0
- package/dist/core/internal/withDedupe.js +12 -0
- package/dist/core/internal/withDedupe.js.map +1 -0
- package/dist/core/zod/request.d.ts +31 -0
- package/dist/core/zod/request.d.ts.map +1 -0
- package/dist/core/zod/request.js +41 -0
- package/dist/core/zod/request.js.map +1 -0
- package/dist/core/zod/rpc.d.ts +603 -0
- package/dist/core/zod/rpc.d.ts.map +1 -0
- package/dist/core/zod/rpc.js +293 -0
- package/dist/core/zod/rpc.js.map +1 -0
- package/dist/core/zod/utils.d.ts +18 -0
- package/dist/core/zod/utils.d.ts.map +1 -0
- package/dist/core/zod/utils.js +21 -0
- package/dist/core/zod/utils.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/types.d.ts +284 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +2 -0
- package/dist/internal/types.js.map +1 -0
- package/dist/server/Handler.d.ts +257 -0
- package/dist/server/Handler.d.ts.map +1 -0
- package/dist/server/Handler.js +433 -0
- package/dist/server/Handler.js.map +1 -0
- package/dist/server/Kv.d.ts +16 -0
- package/dist/server/Kv.d.ts.map +1 -0
- package/dist/server/Kv.js +30 -0
- package/dist/server/Kv.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/internal/requestListener.d.ts +124 -0
- package/dist/server/internal/requestListener.d.ts.map +1 -0
- package/dist/server/internal/requestListener.js +173 -0
- package/dist/server/internal/requestListener.js.map +1 -0
- package/dist/wagmi/Connector.d.ts +93 -0
- package/dist/wagmi/Connector.d.ts.map +1 -0
- package/dist/wagmi/Connector.js +238 -0
- package/dist/wagmi/Connector.js.map +1 -0
- package/dist/wagmi/index.d.ts +3 -0
- package/dist/wagmi/index.d.ts.map +1 -0
- package/dist/wagmi/index.js +3 -0
- package/dist/wagmi/index.js.map +1 -0
- package/package.json +56 -2
- package/src/core/AccessKey.test.ts +257 -0
- package/src/core/AccessKey.ts +123 -0
- package/src/core/Account.test.ts +309 -0
- package/src/core/Account.ts +152 -0
- package/src/core/Adapter.ts +238 -0
- package/src/core/Ceremony.browser.test.ts +239 -0
- package/src/core/Ceremony.test.ts +151 -0
- package/src/core/Ceremony.ts +203 -0
- package/src/core/Client.ts +36 -0
- package/src/core/Dialog.browser.test.ts +309 -0
- package/src/core/Dialog.test-d.ts +19 -0
- package/src/core/Dialog.ts +442 -0
- package/src/core/Expiry.ts +34 -0
- package/src/core/Messenger.ts +206 -0
- package/src/core/Provider.browser.test.ts +774 -0
- package/src/core/Provider.connect.browser.test.ts +415 -0
- package/src/core/Provider.test-d.ts +53 -0
- package/src/core/Provider.test.ts +1566 -0
- package/src/core/Provider.ts +559 -0
- package/src/core/Remote.ts +262 -0
- package/src/core/Schema.test-d.ts +211 -0
- package/src/core/Schema.ts +143 -0
- package/src/core/Storage.ts +213 -0
- package/src/core/Store.test.ts +287 -0
- package/src/core/Store.ts +129 -0
- package/src/core/adapters/dangerous_secp256k1.ts +53 -0
- package/src/core/adapters/dialog.ts +379 -0
- package/src/core/adapters/local.test.ts +97 -0
- package/src/core/adapters/local.ts +277 -0
- package/src/core/adapters/webAuthn.ts +129 -0
- package/src/core/internal/withDedupe.test.ts +116 -0
- package/src/core/internal/withDedupe.ts +20 -0
- package/src/core/mppx.test.ts +83 -0
- package/src/core/zod/request.test.ts +121 -0
- package/src/core/zod/request.ts +70 -0
- package/src/core/zod/rpc.ts +374 -0
- package/src/core/zod/utils.test.ts +69 -0
- package/src/core/zod/utils.ts +40 -0
- package/src/index.ts +14 -0
- package/src/internal/types.ts +378 -0
- package/src/server/Handler.test.ts +1014 -0
- package/src/server/Handler.ts +605 -0
- package/src/server/Kv.ts +46 -0
- package/src/server/index.ts +2 -0
- package/src/server/internal/requestListener.ts +273 -0
- package/src/tsconfig.json +9 -0
- package/src/wagmi/Connector.ts +287 -0
- package/src/wagmi/index.ts +2 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { requestProviders } from 'mipd'
|
|
2
|
+
import { Hex } from 'ox'
|
|
3
|
+
import { type Address, parseUnits } from 'viem'
|
|
4
|
+
import { verifyHash, verifyMessage, verifyTypedData } from 'viem/actions'
|
|
5
|
+
import { Actions, Addresses } from 'viem/tempo'
|
|
6
|
+
import { describe, expect, test } from 'vp/test'
|
|
7
|
+
|
|
8
|
+
import { accounts, chain, getClient } from '../../test/config.js'
|
|
9
|
+
import { url as webauthnUrl } from '../../test/webauthn.constants.js'
|
|
10
|
+
import { webAuthn } from './adapters/webAuthn.js'
|
|
11
|
+
import * as Ceremony from './Ceremony.js'
|
|
12
|
+
import * as Expiry from './Expiry.js'
|
|
13
|
+
import * as Provider from './Provider.js'
|
|
14
|
+
import * as Storage from './Storage.js'
|
|
15
|
+
|
|
16
|
+
const ceremony = Ceremony.server({ url: webauthnUrl })
|
|
17
|
+
|
|
18
|
+
function getProvider(options: Partial<Provider.create.Options> = {}) {
|
|
19
|
+
return Provider.create({
|
|
20
|
+
storage: Storage.idb({ key: crypto.randomUUID() }),
|
|
21
|
+
adapter: webAuthn({ ceremony }),
|
|
22
|
+
...options,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const transferCall = Actions.token.transfer.call({
|
|
27
|
+
to: '0x0000000000000000000000000000000000000001',
|
|
28
|
+
token: Addresses.pathUsd,
|
|
29
|
+
amount: parseUnits('1', 6),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('create', () => {
|
|
33
|
+
test('default: returns an EIP-1193 provider', async () => {
|
|
34
|
+
const provider = getProvider()
|
|
35
|
+
expect(provider).toBeDefined()
|
|
36
|
+
expect(typeof provider.request).toMatchInlineSnapshot(`"function"`)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('eth_chainId', () => {
|
|
41
|
+
test('default: returns configured chain ID as hex', async () => {
|
|
42
|
+
const provider = getProvider()
|
|
43
|
+
|
|
44
|
+
const chainId = await provider.request({ method: 'eth_chainId' })
|
|
45
|
+
expect(chainId).toMatchInlineSnapshot(`"0x1079"`)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('eth_requestAccounts', () => {
|
|
50
|
+
test('default: returns accounts after registering', async () => {
|
|
51
|
+
const provider = getProvider()
|
|
52
|
+
|
|
53
|
+
await provider.request({
|
|
54
|
+
method: 'wallet_connect',
|
|
55
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const accts = await provider.request({ method: 'eth_requestAccounts' })
|
|
59
|
+
expect(accts).toHaveLength(1)
|
|
60
|
+
expect(accts[0]).toMatch(/^0x[0-9a-fA-F]{40}$/)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('wallet_connect', () => {
|
|
65
|
+
test('default: register returns ERC-7846 response', async () => {
|
|
66
|
+
const provider = getProvider()
|
|
67
|
+
|
|
68
|
+
const result = await provider.request({
|
|
69
|
+
method: 'wallet_connect',
|
|
70
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
71
|
+
})
|
|
72
|
+
expect(result.accounts).toHaveLength(1)
|
|
73
|
+
expect(result.accounts[0]!.address).toMatch(/^0x[0-9a-fA-F]{40}$/)
|
|
74
|
+
expect(result.accounts[0]!.capabilities).toMatchInlineSnapshot(`{}`)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('behavior: register preserves existing accounts', async () => {
|
|
78
|
+
const provider = getProvider()
|
|
79
|
+
|
|
80
|
+
await provider.request({
|
|
81
|
+
method: 'wallet_connect',
|
|
82
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
83
|
+
})
|
|
84
|
+
const result = await provider.request({
|
|
85
|
+
method: 'wallet_connect',
|
|
86
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(result.accounts.length).toMatchInlineSnapshot(`2`)
|
|
90
|
+
// New account is active (first) and differs from the loaded one
|
|
91
|
+
expect(result.accounts[0]!.address).not.toBe(result.accounts[1]!.address)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('behavior: login with digest returns signature in capabilities', async () => {
|
|
95
|
+
const provider = getProvider({ chains: [chain] })
|
|
96
|
+
|
|
97
|
+
// Register first to create a credential
|
|
98
|
+
await provider.request({
|
|
99
|
+
method: 'wallet_connect',
|
|
100
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
101
|
+
})
|
|
102
|
+
await provider.request({ method: 'wallet_disconnect' })
|
|
103
|
+
|
|
104
|
+
const result = await provider.request({
|
|
105
|
+
method: 'wallet_connect',
|
|
106
|
+
params: [{ capabilities: { digest: '0xdeadbeef' } }],
|
|
107
|
+
})
|
|
108
|
+
expect(result.accounts[0]!.capabilities.signature).toMatch(/^0x[0-9a-f]+$/)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('behavior: digest signature is verifiable on-chain', async () => {
|
|
112
|
+
const provider = getProvider({ chains: [chain] })
|
|
113
|
+
|
|
114
|
+
// Register and fund
|
|
115
|
+
const regResult = await provider.request({
|
|
116
|
+
method: 'wallet_connect',
|
|
117
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
118
|
+
})
|
|
119
|
+
const address = regResult.accounts[0]!.address
|
|
120
|
+
await fundAccount(address)
|
|
121
|
+
|
|
122
|
+
// Disconnect and login with digest
|
|
123
|
+
await provider.request({ method: 'wallet_disconnect' })
|
|
124
|
+
const digest = '0x00000000000000000000000000000000000000000000000000000000deadbeef' as const
|
|
125
|
+
const result = await provider.request({
|
|
126
|
+
method: 'wallet_connect',
|
|
127
|
+
params: [{ capabilities: { digest } }],
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const client = provider.getClient()
|
|
131
|
+
const valid = await verifyHash(client, {
|
|
132
|
+
address,
|
|
133
|
+
hash: digest,
|
|
134
|
+
signature: result.accounts[0]!.capabilities.signature!,
|
|
135
|
+
})
|
|
136
|
+
expect(valid).toMatchInlineSnapshot(`true`)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('behavior: login without digest returns empty capabilities', async () => {
|
|
140
|
+
const provider = getProvider()
|
|
141
|
+
|
|
142
|
+
// Register first so a credential exists for login
|
|
143
|
+
await provider.request({
|
|
144
|
+
method: 'wallet_connect',
|
|
145
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
146
|
+
})
|
|
147
|
+
await provider.request({ method: 'wallet_disconnect' })
|
|
148
|
+
|
|
149
|
+
const result = await provider.request({ method: 'wallet_connect' })
|
|
150
|
+
expect(result.accounts[0]!.capabilities).toMatchInlineSnapshot(`{}`)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('behavior: register without digest returns empty capabilities', async () => {
|
|
154
|
+
const provider = getProvider()
|
|
155
|
+
|
|
156
|
+
const result = await provider.request({
|
|
157
|
+
method: 'wallet_connect',
|
|
158
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
159
|
+
})
|
|
160
|
+
expect(result.accounts[0]!.capabilities).toMatchInlineSnapshot(`{}`)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('behavior: register with digest returns signature in capabilities', async () => {
|
|
164
|
+
const provider = getProvider({ chains: [chain] })
|
|
165
|
+
|
|
166
|
+
const result = await provider.request({
|
|
167
|
+
method: 'wallet_connect',
|
|
168
|
+
params: [{ capabilities: { method: 'register', digest: '0xdeadbeef' } }],
|
|
169
|
+
})
|
|
170
|
+
expect(result.accounts[0]!.capabilities.signature).toMatch(/^0x[0-9a-f]+$/)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('behavior: register digest signature is verifiable on-chain', async () => {
|
|
174
|
+
const provider = getProvider({ chains: [chain] })
|
|
175
|
+
|
|
176
|
+
const digest = '0x00000000000000000000000000000000000000000000000000000000deadbeef' as const
|
|
177
|
+
const result = await provider.request({
|
|
178
|
+
method: 'wallet_connect',
|
|
179
|
+
params: [{ capabilities: { method: 'register', digest } }],
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const client = provider.getClient()
|
|
183
|
+
const valid = await verifyHash(client, {
|
|
184
|
+
address: result.accounts[0]!.address,
|
|
185
|
+
hash: digest,
|
|
186
|
+
signature: result.accounts[0]!.capabilities.signature!,
|
|
187
|
+
})
|
|
188
|
+
expect(valid).toMatchInlineSnapshot(`true`)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('behavior: signature only on signer account, not others', async () => {
|
|
192
|
+
const provider = getProvider({ chains: [chain] })
|
|
193
|
+
|
|
194
|
+
// Register first, then login with digest
|
|
195
|
+
await provider.request({
|
|
196
|
+
method: 'wallet_connect',
|
|
197
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
198
|
+
})
|
|
199
|
+
await provider.request({ method: 'wallet_disconnect' })
|
|
200
|
+
|
|
201
|
+
// Login creates a new credential, register another
|
|
202
|
+
await provider.request({ method: 'wallet_connect' })
|
|
203
|
+
await provider.request({
|
|
204
|
+
method: 'wallet_connect',
|
|
205
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Now login with digest — signature only on the loaded account
|
|
209
|
+
const result = await provider.request({
|
|
210
|
+
method: 'wallet_connect',
|
|
211
|
+
params: [{ capabilities: { digest: '0xabcd' } }],
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const withSig = result.accounts.filter((a) => a.capabilities.signature)
|
|
215
|
+
const withoutSig = result.accounts.filter((a) => !a.capabilities.signature)
|
|
216
|
+
expect(withSig).toHaveLength(1)
|
|
217
|
+
expect(withSig[0]!.capabilities.signature).toMatch(/^0x[0-9a-f]+$/)
|
|
218
|
+
expect(withoutSig.length).toBeGreaterThanOrEqual(1)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('behavior: login deduplicates and sets active account', async () => {
|
|
222
|
+
const provider = getProvider()
|
|
223
|
+
|
|
224
|
+
// Register then login with same loadAccounts
|
|
225
|
+
await provider.request({
|
|
226
|
+
method: 'wallet_connect',
|
|
227
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
228
|
+
})
|
|
229
|
+
await provider.request({ method: 'wallet_connect' })
|
|
230
|
+
|
|
231
|
+
// Deduplicated (same credential returned on login)
|
|
232
|
+
expect(provider.store.getState().accounts.length).toMatchInlineSnapshot(`1`)
|
|
233
|
+
// Active is the loaded account (returned first by eth_accounts)
|
|
234
|
+
const { activeAccount } = provider.store.getState()
|
|
235
|
+
const loadedAddress = provider.store.getState().accounts[activeAccount]!.address
|
|
236
|
+
const result = await provider.request({ method: 'eth_accounts' })
|
|
237
|
+
expect(result[0]).toBe(loadedAddress)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
describe('wallet_disconnect', () => {
|
|
242
|
+
test('default: clears state', async () => {
|
|
243
|
+
const provider = getProvider()
|
|
244
|
+
|
|
245
|
+
await connect(provider)
|
|
246
|
+
await provider.request({ method: 'wallet_disconnect' })
|
|
247
|
+
|
|
248
|
+
expect(provider.store.getState().accounts).toMatchInlineSnapshot(`[]`)
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('events', () => {
|
|
253
|
+
test('behavior: does not emit accountsChanged on duplicate login', async () => {
|
|
254
|
+
const provider = getProvider()
|
|
255
|
+
|
|
256
|
+
await connect(provider)
|
|
257
|
+
|
|
258
|
+
const events: unknown[] = []
|
|
259
|
+
provider.on('accountsChanged', (accts) => events.push(accts))
|
|
260
|
+
|
|
261
|
+
await provider.request({ method: 'wallet_connect' })
|
|
262
|
+
|
|
263
|
+
expect(events).toMatchInlineSnapshot(`[]`)
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('eth_sendTransaction', () => {
|
|
268
|
+
test('default: sends transaction and returns hash', async () => {
|
|
269
|
+
const provider = getProvider({ chains: [chain] })
|
|
270
|
+
|
|
271
|
+
const address = await connect(provider)
|
|
272
|
+
await fundAccount(address)
|
|
273
|
+
|
|
274
|
+
const hash = await provider.request({
|
|
275
|
+
method: 'eth_sendTransaction',
|
|
276
|
+
params: [{ calls: [transferCall] }],
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
expect(hash).toMatch(/^0x[0-9a-f]{64}$/)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('eth_sendTransactionSync', () => {
|
|
284
|
+
test('default: sends transaction and returns receipt', async () => {
|
|
285
|
+
const provider = getProvider({ chains: [chain] })
|
|
286
|
+
|
|
287
|
+
const address = await connect(provider)
|
|
288
|
+
await fundAccount(address)
|
|
289
|
+
|
|
290
|
+
const receipt = await provider.request({
|
|
291
|
+
method: 'eth_sendTransactionSync',
|
|
292
|
+
params: [{ calls: [transferCall] }],
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const {
|
|
296
|
+
blockHash,
|
|
297
|
+
blockNumber,
|
|
298
|
+
cumulativeGasUsed,
|
|
299
|
+
effectiveGasPrice,
|
|
300
|
+
gasUsed,
|
|
301
|
+
logs,
|
|
302
|
+
logsBloom,
|
|
303
|
+
transactionHash,
|
|
304
|
+
transactionIndex,
|
|
305
|
+
from,
|
|
306
|
+
to,
|
|
307
|
+
feePayer,
|
|
308
|
+
...rest
|
|
309
|
+
} = receipt
|
|
310
|
+
expect(blockHash).toBeDefined()
|
|
311
|
+
expect(blockNumber).toBeDefined()
|
|
312
|
+
expect(cumulativeGasUsed).toBeDefined()
|
|
313
|
+
expect(effectiveGasPrice).toBeDefined()
|
|
314
|
+
expect(gasUsed).toBeDefined()
|
|
315
|
+
expect(logs).toBeInstanceOf(Array)
|
|
316
|
+
expect(logsBloom).toBeDefined()
|
|
317
|
+
expect(transactionHash).toBeDefined()
|
|
318
|
+
expect(transactionIndex).toBeDefined()
|
|
319
|
+
expect(from).toMatch(/^0x[0-9a-f]{40}$/)
|
|
320
|
+
expect(to).toMatch(/^0x[0-9a-f]{40}$/)
|
|
321
|
+
expect(feePayer).toMatch(/^0x[0-9a-f]{40}$/)
|
|
322
|
+
expect(rest).toMatchInlineSnapshot(`
|
|
323
|
+
{
|
|
324
|
+
"contractAddress": null,
|
|
325
|
+
"feeToken": "0x20c0000000000000000000000000000000000000",
|
|
326
|
+
"status": "0x1",
|
|
327
|
+
"type": "0x76",
|
|
328
|
+
}
|
|
329
|
+
`)
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
describe('eth_signTransaction', () => {
|
|
334
|
+
test('default: signs transaction and returns serialized', async () => {
|
|
335
|
+
const provider = getProvider({ chains: [chain] })
|
|
336
|
+
|
|
337
|
+
const address = await connect(provider)
|
|
338
|
+
await fundAccount(address)
|
|
339
|
+
|
|
340
|
+
const signed = await provider.request({
|
|
341
|
+
method: 'eth_signTransaction',
|
|
342
|
+
params: [{ calls: [transferCall] }],
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
expect(signed).toMatch(/^0x/)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test('behavior: signed transaction can be sent via eth_sendRawTransactionSync', async () => {
|
|
349
|
+
const provider = getProvider({ chains: [chain] })
|
|
350
|
+
|
|
351
|
+
const address = await connect(provider)
|
|
352
|
+
await fundAccount(address)
|
|
353
|
+
|
|
354
|
+
const signed = await provider.request({
|
|
355
|
+
method: 'eth_signTransaction',
|
|
356
|
+
params: [{ calls: [transferCall] }],
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const receipt = await provider.request({
|
|
360
|
+
method: 'eth_sendRawTransactionSync',
|
|
361
|
+
params: [signed],
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
expect(receipt.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
|
|
365
|
+
expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
describe('wallet_sendCalls', () => {
|
|
370
|
+
test('default: sends calls and returns id', async () => {
|
|
371
|
+
const provider = getProvider({ chains: [chain] })
|
|
372
|
+
|
|
373
|
+
const address = await connect(provider)
|
|
374
|
+
await fundAccount(address)
|
|
375
|
+
|
|
376
|
+
const result = await provider.request({
|
|
377
|
+
method: 'wallet_sendCalls',
|
|
378
|
+
params: [
|
|
379
|
+
{
|
|
380
|
+
calls: [transferCall],
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
expect(result.id).toMatch(/^0x[0-9a-f]+$/)
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('behavior: with sync capability returns id and sync capability', async () => {
|
|
389
|
+
const provider = getProvider({ chains: [chain] })
|
|
390
|
+
|
|
391
|
+
const address = await connect(provider)
|
|
392
|
+
await fundAccount(address)
|
|
393
|
+
|
|
394
|
+
const result = await provider.request({
|
|
395
|
+
method: 'wallet_sendCalls',
|
|
396
|
+
params: [
|
|
397
|
+
{
|
|
398
|
+
calls: [transferCall],
|
|
399
|
+
capabilities: { sync: true },
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
expect(result.id).toMatch(/^0x[0-9a-f]+$/)
|
|
405
|
+
expect(result.capabilities).toMatchInlineSnapshot(`
|
|
406
|
+
{
|
|
407
|
+
"sync": true,
|
|
408
|
+
}
|
|
409
|
+
`)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('behavior: sync false uses async path', async () => {
|
|
413
|
+
const provider = getProvider({ chains: [chain] })
|
|
414
|
+
|
|
415
|
+
const address = await connect(provider)
|
|
416
|
+
await fundAccount(address)
|
|
417
|
+
|
|
418
|
+
const result = await provider.request({
|
|
419
|
+
method: 'wallet_sendCalls',
|
|
420
|
+
params: [
|
|
421
|
+
{
|
|
422
|
+
calls: [transferCall],
|
|
423
|
+
capabilities: { sync: false },
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
expect(result.id).toMatch(/^0x[0-9a-f]+$/)
|
|
429
|
+
expect(result.capabilities).toMatchInlineSnapshot(`
|
|
430
|
+
{
|
|
431
|
+
"sync": false,
|
|
432
|
+
}
|
|
433
|
+
`)
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
describe('personal_sign', () => {
|
|
438
|
+
test('default: signs a message and returns signature', async () => {
|
|
439
|
+
const provider = getProvider({ chains: [chain] })
|
|
440
|
+
|
|
441
|
+
const address = await connect(provider)
|
|
442
|
+
|
|
443
|
+
const message = Hex.fromString('hello world')
|
|
444
|
+
const signature = await provider.request({
|
|
445
|
+
method: 'personal_sign',
|
|
446
|
+
params: [message, address],
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
expect(signature).toMatch(/^0x[0-9a-f]+$/)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
test('behavior: signature is verifiable on-chain', async () => {
|
|
453
|
+
const provider = getProvider({ chains: [chain] })
|
|
454
|
+
|
|
455
|
+
const address = await connect(provider)
|
|
456
|
+
await fundAccount(address)
|
|
457
|
+
|
|
458
|
+
const message = Hex.fromString('hello world')
|
|
459
|
+
const signature = await provider.request({
|
|
460
|
+
method: 'personal_sign',
|
|
461
|
+
params: [message, address],
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const client = provider.getClient()
|
|
465
|
+
const valid = await verifyMessage(client, {
|
|
466
|
+
address,
|
|
467
|
+
message: { raw: message },
|
|
468
|
+
signature,
|
|
469
|
+
})
|
|
470
|
+
expect(valid).toMatchInlineSnapshot(`true`)
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
describe('eth_signTypedData_v4', () => {
|
|
475
|
+
const typedData = {
|
|
476
|
+
domain: { name: 'Test', version: '1', chainId: 1 },
|
|
477
|
+
types: {
|
|
478
|
+
Person: [
|
|
479
|
+
{ name: 'name', type: 'string' },
|
|
480
|
+
{ name: 'wallet', type: 'address' },
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
primaryType: 'Person' as const,
|
|
484
|
+
message: { name: 'Bob', wallet: '0x0000000000000000000000000000000000000000' },
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
test('default: signs typed data and returns signature', async () => {
|
|
488
|
+
const provider = getProvider({ chains: [chain] })
|
|
489
|
+
|
|
490
|
+
const address = await connect(provider)
|
|
491
|
+
|
|
492
|
+
const signature = await provider.request({
|
|
493
|
+
method: 'eth_signTypedData_v4',
|
|
494
|
+
params: [address, JSON.stringify(typedData)],
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
expect(signature).toMatch(/^0x[0-9a-f]+$/)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test('behavior: signature is verifiable on-chain', async () => {
|
|
501
|
+
const provider = getProvider({ chains: [chain] })
|
|
502
|
+
|
|
503
|
+
const address = await connect(provider)
|
|
504
|
+
await fundAccount(address)
|
|
505
|
+
|
|
506
|
+
const signature = await provider.request({
|
|
507
|
+
method: 'eth_signTypedData_v4',
|
|
508
|
+
params: [address, JSON.stringify(typedData)],
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
const client = provider.getClient()
|
|
512
|
+
const valid = await verifyTypedData(client, {
|
|
513
|
+
address,
|
|
514
|
+
signature,
|
|
515
|
+
...typedData,
|
|
516
|
+
})
|
|
517
|
+
expect(valid).toMatchInlineSnapshot(`true`)
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
describe('wallet_authorizeAccessKey', () => {
|
|
522
|
+
test('default: grants an access key and returns its address', async () => {
|
|
523
|
+
const provider = getProvider({ chains: [chain] })
|
|
524
|
+
await connect(provider)
|
|
525
|
+
|
|
526
|
+
const result = await provider.request({
|
|
527
|
+
method: 'wallet_authorizeAccessKey',
|
|
528
|
+
params: [{ expiry: Expiry.days(1) }],
|
|
529
|
+
})
|
|
530
|
+
expect(result.address).toMatch(/^0x[0-9a-fA-F]{40}$/)
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
test('behavior: granted access key is used for sendTransactionSync', async () => {
|
|
534
|
+
const provider = getProvider({ chains: [chain] })
|
|
535
|
+
const address = await connect(provider)
|
|
536
|
+
await fundAccount(address)
|
|
537
|
+
|
|
538
|
+
await provider.request({
|
|
539
|
+
method: 'wallet_authorizeAccessKey',
|
|
540
|
+
params: [{ expiry: Expiry.days(1) }],
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
const receipt = await provider.request({
|
|
544
|
+
method: 'eth_sendTransactionSync',
|
|
545
|
+
params: [{ calls: [transferCall] }],
|
|
546
|
+
})
|
|
547
|
+
expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
test('behavior: with expiry option', async () => {
|
|
551
|
+
const provider = getProvider({ chains: [chain] })
|
|
552
|
+
await connect(provider)
|
|
553
|
+
|
|
554
|
+
const expiry = Math.floor(Date.now() / 1000) + 3600
|
|
555
|
+
const result = await provider.request({
|
|
556
|
+
method: 'wallet_authorizeAccessKey',
|
|
557
|
+
params: [{ expiry }],
|
|
558
|
+
})
|
|
559
|
+
expect(result.address).toMatch(/^0x[0-9a-fA-F]{40}$/)
|
|
560
|
+
expect(result.expiry).toBe(Hex.fromNumber(expiry))
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
test('behavior: with limits option', async () => {
|
|
564
|
+
const provider = getProvider({ chains: [chain] })
|
|
565
|
+
const address = await connect(provider)
|
|
566
|
+
await fundAccount(address)
|
|
567
|
+
|
|
568
|
+
const result = await provider.request({
|
|
569
|
+
method: 'wallet_authorizeAccessKey',
|
|
570
|
+
params: [
|
|
571
|
+
{
|
|
572
|
+
expiry: Expiry.days(1),
|
|
573
|
+
limits: [{ token: Addresses.pathUsd, limit: Hex.fromNumber(parseUnits('5', 6)) }],
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
})
|
|
577
|
+
expect(result.limits).toMatchInlineSnapshot(`
|
|
578
|
+
[
|
|
579
|
+
{
|
|
580
|
+
"limit": "0x4c4b40",
|
|
581
|
+
"token": "0x20c0000000000000000000000000000000000000",
|
|
582
|
+
},
|
|
583
|
+
]
|
|
584
|
+
`)
|
|
585
|
+
|
|
586
|
+
const receipt = await provider.request({
|
|
587
|
+
method: 'eth_sendTransactionSync',
|
|
588
|
+
params: [{ calls: [transferCall] }],
|
|
589
|
+
})
|
|
590
|
+
expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
|
|
591
|
+
})
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
describe('wallet_revokeAccessKey', () => {
|
|
595
|
+
test('default: revokes a granted access key', async () => {
|
|
596
|
+
const provider = getProvider({ chains: [chain] })
|
|
597
|
+
await connect(provider)
|
|
598
|
+
|
|
599
|
+
const connected = (await provider.request({ method: 'eth_accounts' }))[0]!
|
|
600
|
+
const { address: keyAddress } = await provider.request({
|
|
601
|
+
method: 'wallet_authorizeAccessKey',
|
|
602
|
+
params: [{ expiry: Expiry.days(1) }],
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
await provider.request({
|
|
606
|
+
method: 'wallet_revokeAccessKey',
|
|
607
|
+
params: [{ address: connected, accessKeyAddress: keyAddress }],
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
await fundAccount(connected)
|
|
611
|
+
|
|
612
|
+
const receipt = await provider.request({
|
|
613
|
+
method: 'eth_sendTransactionSync',
|
|
614
|
+
params: [{ calls: [transferCall] }],
|
|
615
|
+
})
|
|
616
|
+
expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
|
|
617
|
+
})
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
describe('wallet_connect with authorizeAccessKey', () => {
|
|
621
|
+
test('default: grants access key during register', async () => {
|
|
622
|
+
const provider = getProvider({ chains: [chain] })
|
|
623
|
+
|
|
624
|
+
const result = await provider.request({
|
|
625
|
+
method: 'wallet_connect',
|
|
626
|
+
params: [
|
|
627
|
+
{
|
|
628
|
+
capabilities: {
|
|
629
|
+
method: 'register',
|
|
630
|
+
authorizeAccessKey: { expiry: Math.floor(Date.now() / 1000) + 3600 },
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
})
|
|
635
|
+
expect(result.accounts.length).toBeGreaterThanOrEqual(1)
|
|
636
|
+
expect(result.accounts[0]!.capabilities.keyAuthorization).toBeDefined()
|
|
637
|
+
expect(result.accounts[0]!.capabilities.keyAuthorization!.address).toMatch(/^0x[0-9a-f]{40}$/i)
|
|
638
|
+
|
|
639
|
+
const address = result.accounts[0]!.address
|
|
640
|
+
await fundAccount(address)
|
|
641
|
+
|
|
642
|
+
const receipt = await provider.request({
|
|
643
|
+
method: 'eth_sendTransactionSync',
|
|
644
|
+
params: [{ calls: [transferCall] }],
|
|
645
|
+
})
|
|
646
|
+
expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
test('behavior: authorizeAccessKey with expiry during register', async () => {
|
|
650
|
+
const provider = getProvider({ chains: [chain] })
|
|
651
|
+
|
|
652
|
+
const expiry = Math.floor(Date.now() / 1000) + 3600
|
|
653
|
+
const result = await provider.request({
|
|
654
|
+
method: 'wallet_connect',
|
|
655
|
+
params: [{ capabilities: { method: 'register', authorizeAccessKey: { expiry } } }],
|
|
656
|
+
})
|
|
657
|
+
expect(result.accounts.length).toBeGreaterThanOrEqual(1)
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
test('behavior: authorizeAccessKey during login', async () => {
|
|
661
|
+
const provider = getProvider({ chains: [chain] })
|
|
662
|
+
|
|
663
|
+
await provider.request({
|
|
664
|
+
method: 'wallet_connect',
|
|
665
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
const result = await provider.request({
|
|
669
|
+
method: 'wallet_connect',
|
|
670
|
+
params: [
|
|
671
|
+
{
|
|
672
|
+
capabilities: { authorizeAccessKey: { expiry: Math.floor(Date.now() / 1000) + 3600 } },
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
})
|
|
676
|
+
expect(result.accounts.length).toBeGreaterThanOrEqual(1)
|
|
677
|
+
|
|
678
|
+
const address = result.accounts[0]!.address
|
|
679
|
+
await fundAccount(address)
|
|
680
|
+
|
|
681
|
+
const receipt = await provider.request({
|
|
682
|
+
method: 'eth_sendTransactionSync',
|
|
683
|
+
params: [{ calls: [transferCall] }],
|
|
684
|
+
})
|
|
685
|
+
expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
test('behavior: authorizeAccessKey with digest during register', async () => {
|
|
689
|
+
const provider = getProvider({ chains: [chain] })
|
|
690
|
+
|
|
691
|
+
const result = await provider.request({
|
|
692
|
+
method: 'wallet_connect',
|
|
693
|
+
params: [
|
|
694
|
+
{
|
|
695
|
+
capabilities: {
|
|
696
|
+
method: 'register',
|
|
697
|
+
digest: '0xdeadbeef',
|
|
698
|
+
authorizeAccessKey: { expiry: Math.floor(Date.now() / 1000) + 3600 },
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
})
|
|
703
|
+
expect(result.accounts[0]!.capabilities.signature).toMatch(/^0x[0-9a-f]+$/)
|
|
704
|
+
expect(result.accounts[0]!.capabilities.keyAuthorization).toBeDefined()
|
|
705
|
+
})
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
describe('eip-6963', () => {
|
|
709
|
+
test('default: announces provider when adapter has icon, name, and rdns', async () => {
|
|
710
|
+
const discovered: { info: { icon: string; name: string; rdns: string; uuid: string } }[] = []
|
|
711
|
+
const unsubscribe = requestProviders((detail) => discovered.push(detail as never))
|
|
712
|
+
|
|
713
|
+
getProvider({
|
|
714
|
+
adapter: webAuthn({
|
|
715
|
+
ceremony,
|
|
716
|
+
icon: 'data:image/svg+xml,<svg></svg>',
|
|
717
|
+
name: 'Test Wallet',
|
|
718
|
+
rdns: 'com.example.test',
|
|
719
|
+
}),
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
// Wait a tick for the event to fire
|
|
723
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
724
|
+
|
|
725
|
+
const match = discovered.find((d) => d.info.rdns === 'com.example.test')!
|
|
726
|
+
expect(match.info.name).toMatchInlineSnapshot(`"Test Wallet"`)
|
|
727
|
+
expect(match.info.rdns).toMatchInlineSnapshot(`"com.example.test"`)
|
|
728
|
+
expect(match.info.icon).toMatchInlineSnapshot(`"data:image/svg+xml,<svg></svg>"`)
|
|
729
|
+
expect(match.info.uuid).toMatch(
|
|
730
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
unsubscribe?.()
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
test('behavior: defaults icon and rdns when not provided', async () => {
|
|
737
|
+
const discovered: { info: { icon: string; name: string; rdns: string } }[] = []
|
|
738
|
+
const unsubscribe = requestProviders((detail) => discovered.push(detail as never))
|
|
739
|
+
|
|
740
|
+
getProvider({
|
|
741
|
+
adapter: webAuthn({ ceremony, name: 'My Wallet' }),
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
745
|
+
|
|
746
|
+
const info = discovered[discovered.length - 1]!.info
|
|
747
|
+
expect(info.name).toMatchInlineSnapshot(`"My Wallet"`)
|
|
748
|
+
expect(info.rdns).toMatchInlineSnapshot(`"com.mywallet"`)
|
|
749
|
+
expect(info.icon).toMatch(/^data:image\/svg\+xml,/)
|
|
750
|
+
|
|
751
|
+
unsubscribe?.()
|
|
752
|
+
})
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
/** Funds an account with fee tokens so it can send transactions. */
|
|
756
|
+
async function fundAccount(address: Address) {
|
|
757
|
+
const client = getClient()
|
|
758
|
+
await Actions.token.transferSync(client, {
|
|
759
|
+
account: accounts[0]!,
|
|
760
|
+
feeToken: Addresses.pathUsd,
|
|
761
|
+
to: address,
|
|
762
|
+
token: Addresses.pathUsd,
|
|
763
|
+
amount: parseUnits('10', 6),
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Registers a new account and returns its address. */
|
|
768
|
+
async function connect(provider: ReturnType<typeof getProvider>) {
|
|
769
|
+
const result = await provider.request({
|
|
770
|
+
method: 'wallet_connect',
|
|
771
|
+
params: [{ capabilities: { method: 'register' } }],
|
|
772
|
+
})
|
|
773
|
+
return result.accounts[0]!.address
|
|
774
|
+
}
|