zerowallet-adapter 1.0.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.
- package/README.md +123 -0
- package/jest.config.js +7 -0
- package/package.json +51 -0
- package/src/adapter.ts +272 -0
- package/src/deeplink.ts +131 -0
- package/src/errors.ts +25 -0
- package/src/index.ts +6 -0
- package/src/standard.ts +172 -0
- package/src/types.ts +53 -0
- package/src/utils.ts +92 -0
- package/tests/adapter.test.ts +142 -0
- package/tests/deeplink.test.ts +103 -0
- package/tests/utils.test.ts +68 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @zerowallet/adapter
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@zerowallet/adapter)
|
|
4
|
+
|
|
5
|
+
Zero Wallet Adapter is the official Solana Wallet Adapter SDK for **Zero Wallet**, a self-custody mobile wallet native to Solana.
|
|
6
|
+
|
|
7
|
+
This adapter allows any Solana dApp using `@solana/wallet-adapter-react` to instantly connect to the Zero Wallet mobile app, enabling secure mobile transaction signing via deep links.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What is Zero Wallet?
|
|
12
|
+
Zero Wallet gives users true self-custody mobile access to Solana. Instead of storing private keys in browser extensions or adapter layers, **Zero Wallet keeps your keys safely encrypted on your mobile device**.
|
|
13
|
+
|
|
14
|
+
When a user connects a dApp or signs a transaction, the `@zerowallet/adapter` passes the request to the Zero Wallet app via secure OS-level deep links (`zerowallet://`), entirely offloading the private key operations to the mobile application.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add the package via your preferred package manager:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @zerowallet/adapter @solana/wallet-adapter-base @solana/web3.js
|
|
22
|
+
# or
|
|
23
|
+
yarn add @zerowallet/adapter @solana/wallet-adapter-base @solana/web3.js
|
|
24
|
+
# or
|
|
25
|
+
pnpm add @zerowallet/adapter @solana/wallet-adapter-base @solana/web3.js
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
Integrating Zero Wallet works exactly like integrating Phantom or Solflare in the standard Solana wallet adapter ecosystem.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import React, { useMemo } from 'react';
|
|
34
|
+
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
|
|
35
|
+
import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui';
|
|
36
|
+
import { ZeroWalletAdapter } from '@zerowallet/adapter';
|
|
37
|
+
import '@solana/wallet-adapter-react-ui/styles.css';
|
|
38
|
+
|
|
39
|
+
export const Wallet = ({ children }) => {
|
|
40
|
+
// Determine your network
|
|
41
|
+
const endpoint = 'https://api.mainnet-beta.solana.com';
|
|
42
|
+
|
|
43
|
+
const wallets = useMemo(
|
|
44
|
+
() => [
|
|
45
|
+
new ZeroWalletAdapter({
|
|
46
|
+
network: 'mainnet-beta', // or 'devnet', 'testnet'
|
|
47
|
+
// Optional: Hardcode the callback URL if you are not running in a browser environment
|
|
48
|
+
// callbackUrl: 'https://my-dapp.com/callback'
|
|
49
|
+
}),
|
|
50
|
+
// ... other wallets like new PhantomWalletAdapter()
|
|
51
|
+
],
|
|
52
|
+
[]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<ConnectionProvider endpoint={endpoint}>
|
|
57
|
+
<WalletProvider wallets={wallets} autoConnect>
|
|
58
|
+
<WalletModalProvider>
|
|
59
|
+
<WalletMultiButton />
|
|
60
|
+
{children}
|
|
61
|
+
</WalletModalProvider>
|
|
62
|
+
</WalletProvider>
|
|
63
|
+
</ConnectionProvider>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Deep Link Flow
|
|
69
|
+
|
|
70
|
+
When a user initiates an action from your dApp, the flow is as follows:
|
|
71
|
+
|
|
72
|
+
1. **Adapter serializes payload**: The adapter encodes the payload (e.g., connection request, serialized transaction) into Base64.
|
|
73
|
+
2. **Deep link dispatched**: The adapter calls `window.location.href = 'zerowallet://...'`.
|
|
74
|
+
3. **OS opens Zero Wallet**: The mobile device triggers the Zero Wallet app and passes the payload.
|
|
75
|
+
4. **User approves in app**: The user sees the transaction UI in Zero Wallet and taps "Approve" (signing securely with local keys).
|
|
76
|
+
5. **App redirects back**: Zero Wallet constructs the callback URL passed by the dApp, appending the signed data or public key, and opens it.
|
|
77
|
+
6. **Adapter resolves**: The adapter code waiting for the callback completes the Promise and hands the signed data back to the dApp.
|
|
78
|
+
|
|
79
|
+
## Deep Link URL Scheme (For App Developers)
|
|
80
|
+
|
|
81
|
+
If you are developing the mobile app side of Zero Wallet, you must intercept the `zerowallet://` scheme and handle these routes:
|
|
82
|
+
|
|
83
|
+
### Connect
|
|
84
|
+
```
|
|
85
|
+
zerowallet://connect?callback=<callback_url>&id=<unique_id>
|
|
86
|
+
```
|
|
87
|
+
**Response Format (Appended to Callback URL):**
|
|
88
|
+
* Success: `<callback_url>?id=<id>&status=approved&publicKey=<base58>`
|
|
89
|
+
* Failure: `<callback_url>?id=<id>&status=rejected&error=User%20Cancelled`
|
|
90
|
+
|
|
91
|
+
### Sign Transaction
|
|
92
|
+
```
|
|
93
|
+
zerowallet://sign?tx=<base64_serialized_tx>&callback=<callback_url>&id=<id>&network=<mainnet-beta|devnet>
|
|
94
|
+
```
|
|
95
|
+
**Response Format:**
|
|
96
|
+
* Success: `<callback_url>?id=<id>&status=approved&signedTx=<base64_signed_tx>`
|
|
97
|
+
|
|
98
|
+
### Sign All Transactions
|
|
99
|
+
```
|
|
100
|
+
zerowallet://signAll?txs=<base64_array_of_txs>&callback=<callback_url>&id=<id>
|
|
101
|
+
```
|
|
102
|
+
**Response Format:**
|
|
103
|
+
* Success: `<callback_url>?id=<id>&status=approved&signedTxs=<base64_array_of_signed_txs>`
|
|
104
|
+
|
|
105
|
+
### Sign Message
|
|
106
|
+
```
|
|
107
|
+
zerowallet://signMessage?msg=<base64_message>&callback=<callback_url>&id=<id>
|
|
108
|
+
```
|
|
109
|
+
**Response Format:**
|
|
110
|
+
* Success: `<callback_url>?id=<id>&status=approved&signature=<base64_signature>`
|
|
111
|
+
|
|
112
|
+
## Troubleshooting
|
|
113
|
+
|
|
114
|
+
### Deep Links are not opening
|
|
115
|
+
* Ensure you are testing on a mobile device where Zero Wallet is installed. Desktop browsers without an extension wrapper will trigger a timeout.
|
|
116
|
+
* Check that your dApp's callback URL is valid and does not drop URL parameters on reload (often happens with heavily cached SPAs).
|
|
117
|
+
|
|
118
|
+
### Callback times out
|
|
119
|
+
* The default timeout is 120 seconds. If the user takes longer to approve within the app, the adapter will throw a `WalletTimeoutError`. Catch this error and prompt the user to try again.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zerowallet-adapter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero Wallet Adapter SDK for Solana dApps",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"test": "jest",
|
|
20
|
+
"lint": "eslint src --ext .ts",
|
|
21
|
+
"clean": "rm -rf dist"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"solana",
|
|
25
|
+
"wallet",
|
|
26
|
+
"adapter",
|
|
27
|
+
"zero",
|
|
28
|
+
"zerowallet"
|
|
29
|
+
],
|
|
30
|
+
"author": "Zero Wallet",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@solana/wallet-adapter-base": "^0.9.23",
|
|
34
|
+
"@solana/web3.js": "^1.87.6"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@solana/wallet-standard-features": "^1.1.0",
|
|
38
|
+
"@wallet-standard/core": "^1.0.3",
|
|
39
|
+
"@wallet-standard/features": "^1.0.3",
|
|
40
|
+
"@wallet-standard/wallet": "^1.0.1",
|
|
41
|
+
"bs58": "^5.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/jest": "^29.5.12",
|
|
45
|
+
"@types/node": "^20.12.7",
|
|
46
|
+
"jest": "^29.7.0",
|
|
47
|
+
"ts-jest": "^29.1.2",
|
|
48
|
+
"tsup": "^8.0.2",
|
|
49
|
+
"typescript": "^5.4.5"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseMessageSignerWalletAdapter,
|
|
3
|
+
EventEmitter,
|
|
4
|
+
WalletName,
|
|
5
|
+
WalletReadyState,
|
|
6
|
+
WalletSignTransactionError,
|
|
7
|
+
WalletSignMessageError,
|
|
8
|
+
WalletConnectionError,
|
|
9
|
+
WalletDisconnectedError,
|
|
10
|
+
WalletTimeoutError
|
|
11
|
+
} from '@solana/wallet-adapter-base';
|
|
12
|
+
import { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
|
|
13
|
+
import {
|
|
14
|
+
ZeroWalletAdapterConfig,
|
|
15
|
+
ConnectCallbackParams,
|
|
16
|
+
SignCallbackParams
|
|
17
|
+
} from './types';
|
|
18
|
+
import {
|
|
19
|
+
ZeroWalletNotInstalledError,
|
|
20
|
+
ZeroWalletConnectionError,
|
|
21
|
+
ZeroWalletUserRejectionError,
|
|
22
|
+
ZeroWalletSignTransactionError,
|
|
23
|
+
ZeroWalletSignMessageError,
|
|
24
|
+
ZeroWalletTimeoutError
|
|
25
|
+
} from './errors';
|
|
26
|
+
import {
|
|
27
|
+
buildConnectUrl,
|
|
28
|
+
buildSignMessageUrl,
|
|
29
|
+
buildSignTransactionUrl,
|
|
30
|
+
buildSignAllUrl,
|
|
31
|
+
isMobile,
|
|
32
|
+
isZeroWalletInstalled,
|
|
33
|
+
openDeepLink,
|
|
34
|
+
waitForCallback,
|
|
35
|
+
parseCallbackParams
|
|
36
|
+
} from './deeplink';
|
|
37
|
+
import {
|
|
38
|
+
generateCallbackId,
|
|
39
|
+
serializeTransaction,
|
|
40
|
+
deserializeTransaction,
|
|
41
|
+
serializeMessage,
|
|
42
|
+
deserializeSignature
|
|
43
|
+
} from './utils';
|
|
44
|
+
import bs58 from 'bs58';
|
|
45
|
+
|
|
46
|
+
export const ZeroWalletName = 'Zero Wallet' as WalletName<'Zero Wallet'>;
|
|
47
|
+
|
|
48
|
+
// Simple clean SVG icon base64 encoded
|
|
49
|
+
export const ZeroWalletIcon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTE2LjUgNy41QzE2Ljc1IDcuNSA5IDIwIDkuNSAyMEw3LjUgMTYuNUM3LjI1IDE2LjUgMTUgNCAxNC41IDRMMTYuNSA3LjVaIiBmaWxsPSJibGFjayIvPjwvc3ZnPg==';
|
|
50
|
+
|
|
51
|
+
export interface ZeroWalletAdapterEvents {
|
|
52
|
+
connect(publicKey: PublicKey): void;
|
|
53
|
+
disconnect(): void;
|
|
54
|
+
error(error: any): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class ZeroWalletAdapter extends BaseMessageSignerWalletAdapter {
|
|
58
|
+
name = ZeroWalletName;
|
|
59
|
+
url = 'https://zerowallet.app';
|
|
60
|
+
icon = ZeroWalletIcon;
|
|
61
|
+
supportedTransactionVersions = new Set(['legacy', 0] as const);
|
|
62
|
+
|
|
63
|
+
private _connecting: boolean;
|
|
64
|
+
private _wallet: any | null; // For standard injected wallet if applicable
|
|
65
|
+
private _publicKey: PublicKey | null;
|
|
66
|
+
private _readyState: WalletReadyState;
|
|
67
|
+
private _config: ZeroWalletAdapterConfig;
|
|
68
|
+
|
|
69
|
+
constructor(config: ZeroWalletAdapterConfig = {}) {
|
|
70
|
+
super();
|
|
71
|
+
this._config = {
|
|
72
|
+
network: config.network || 'mainnet-beta',
|
|
73
|
+
timeoutMs: config.timeoutMs || 120000,
|
|
74
|
+
callbackUrl: config.callbackUrl || (typeof window !== 'undefined' ? window.location.href : '')
|
|
75
|
+
};
|
|
76
|
+
this._connecting = false;
|
|
77
|
+
this._wallet = null;
|
|
78
|
+
this._publicKey = null;
|
|
79
|
+
this._readyState = WalletReadyState.Unsupported;
|
|
80
|
+
|
|
81
|
+
// Determine ready state
|
|
82
|
+
if (typeof window !== 'undefined') {
|
|
83
|
+
if (isMobile()) {
|
|
84
|
+
// On mobile we consider it installed/loadable via deep links
|
|
85
|
+
this._readyState = isZeroWalletInstalled() ? WalletReadyState.Installed : WalletReadyState.Loadable;
|
|
86
|
+
} else if (isZeroWalletInstalled()) {
|
|
87
|
+
this._readyState = WalletReadyState.Installed;
|
|
88
|
+
} else {
|
|
89
|
+
this._readyState = WalletReadyState.Loadable; // Fallback to QR / WalletConnect eventually
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Try to recover session from local storage
|
|
94
|
+
if (typeof window !== 'undefined') {
|
|
95
|
+
try {
|
|
96
|
+
const storedKey = localStorage.getItem('zerowallet_pubkey');
|
|
97
|
+
if (storedKey) {
|
|
98
|
+
this._publicKey = new PublicKey(storedKey);
|
|
99
|
+
// Emit connect on next tick so listeners have time to attach
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
if (this._publicKey) this.emit('connect', this._publicKey);
|
|
102
|
+
}, 0);
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// Ignore storage errors
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get publicKey(): PublicKey | null {
|
|
111
|
+
return this._publicKey;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get connecting(): boolean {
|
|
115
|
+
return this._connecting;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get readyState(): WalletReadyState {
|
|
119
|
+
return this._readyState;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async connect(): Promise<void> {
|
|
123
|
+
try {
|
|
124
|
+
if (this.connected || this.connecting) return;
|
|
125
|
+
|
|
126
|
+
if (this._readyState !== WalletReadyState.Installed && this._readyState !== WalletReadyState.Loadable) {
|
|
127
|
+
throw new ZeroWalletNotInstalledError();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this._connecting = true;
|
|
131
|
+
|
|
132
|
+
const id = generateCallbackId();
|
|
133
|
+
const deepLinkUrl = buildConnectUrl(this._config.callbackUrl!, id);
|
|
134
|
+
|
|
135
|
+
openDeepLink(deepLinkUrl);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// Wait for the app to redirect back with the params
|
|
139
|
+
const params = await waitForCallback(id, this._config.timeoutMs);
|
|
140
|
+
const parsed = parseCallbackParams(params) as ConnectCallbackParams;
|
|
141
|
+
|
|
142
|
+
if (parsed.status === 'rejected') {
|
|
143
|
+
throw new ZeroWalletUserRejectionError(parsed.error || 'User rejected the connection request.');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!parsed.publicKey) {
|
|
147
|
+
throw new ZeroWalletConnectionError('No public key returned from wallet.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this._publicKey = new PublicKey(parsed.publicKey);
|
|
151
|
+
|
|
152
|
+
// Save session
|
|
153
|
+
if (typeof window !== 'undefined') {
|
|
154
|
+
localStorage.setItem('zerowallet_pubkey', this._publicKey.toBase58());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.emit('connect', this._publicKey);
|
|
158
|
+
} catch (e: any) {
|
|
159
|
+
if (e instanceof ZeroWalletTimeoutError) {
|
|
160
|
+
throw new WalletTimeoutError(e?.message, e);
|
|
161
|
+
}
|
|
162
|
+
throw e; // rethrow mapping error
|
|
163
|
+
}
|
|
164
|
+
} catch (error: any) {
|
|
165
|
+
this.emit('error', error);
|
|
166
|
+
throw error;
|
|
167
|
+
} finally {
|
|
168
|
+
this._connecting = false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async disconnect(): Promise<void> {
|
|
173
|
+
this._publicKey = null;
|
|
174
|
+
if (typeof window !== 'undefined') {
|
|
175
|
+
localStorage.removeItem('zerowallet_pubkey');
|
|
176
|
+
}
|
|
177
|
+
this.emit('disconnect');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async signTransaction<T extends Transaction | VersionedTransaction>(transaction: T): Promise<T> {
|
|
181
|
+
try {
|
|
182
|
+
if (!this.connected) throw new WalletDisconnectedError();
|
|
183
|
+
|
|
184
|
+
const id = generateCallbackId();
|
|
185
|
+
const serialized = serializeTransaction(transaction);
|
|
186
|
+
const url = buildSignTransactionUrl(serialized, this._config.callbackUrl!, id, this._config.network);
|
|
187
|
+
|
|
188
|
+
openDeepLink(url);
|
|
189
|
+
|
|
190
|
+
const params = await waitForCallback(id, this._config.timeoutMs);
|
|
191
|
+
const parsed = parseCallbackParams(params) as SignCallbackParams;
|
|
192
|
+
|
|
193
|
+
if (parsed.status === 'rejected') {
|
|
194
|
+
throw new ZeroWalletUserRejectionError(parsed.error || 'User rejected signature.');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!parsed.signedTx) {
|
|
198
|
+
throw new ZeroWalletSignTransactionError('No signed transaction returned.');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return deserializeTransaction(parsed.signedTx) as T;
|
|
202
|
+
} catch (error: any) {
|
|
203
|
+
this.emit('error', error);
|
|
204
|
+
if (error instanceof ZeroWalletTimeoutError) {
|
|
205
|
+
throw new WalletTimeoutError(error?.message, error);
|
|
206
|
+
}
|
|
207
|
+
throw new WalletSignTransactionError(error?.message, error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async signAllTransactions<T extends Transaction | VersionedTransaction>(transactions: T[]): Promise<T[]> {
|
|
212
|
+
try {
|
|
213
|
+
if (!this.connected) throw new WalletDisconnectedError();
|
|
214
|
+
|
|
215
|
+
const id = generateCallbackId();
|
|
216
|
+
const serializedTxs = transactions.map((t: T) => serializeTransaction(t));
|
|
217
|
+
const url = buildSignAllUrl(serializedTxs, this._config.callbackUrl!, id, this._config.network);
|
|
218
|
+
|
|
219
|
+
openDeepLink(url);
|
|
220
|
+
|
|
221
|
+
const params = await waitForCallback(id, this._config.timeoutMs);
|
|
222
|
+
const parsed = parseCallbackParams(params) as SignCallbackParams;
|
|
223
|
+
|
|
224
|
+
if (parsed.status === 'rejected') {
|
|
225
|
+
throw new ZeroWalletUserRejectionError(parsed.error || 'User rejected signature.');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!parsed.signedTxs || parsed.signedTxs.length !== transactions.length) {
|
|
229
|
+
throw new ZeroWalletSignTransactionError('Invalid number of signed transactions returned.');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return parsed.signedTxs.map(t => deserializeTransaction(t) as T);
|
|
233
|
+
} catch (error: any) {
|
|
234
|
+
this.emit('error', error);
|
|
235
|
+
if (error instanceof ZeroWalletTimeoutError) {
|
|
236
|
+
throw new WalletTimeoutError(error?.message, error);
|
|
237
|
+
}
|
|
238
|
+
throw new WalletSignTransactionError(error?.message, error);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async signMessage(message: Uint8Array): Promise<Uint8Array> {
|
|
243
|
+
try {
|
|
244
|
+
if (!this.connected || !this.publicKey) throw new WalletDisconnectedError();
|
|
245
|
+
|
|
246
|
+
const id = generateCallbackId();
|
|
247
|
+
const serializedMessage = serializeMessage(message);
|
|
248
|
+
const url = buildSignMessageUrl(serializedMessage, this._config.callbackUrl!, id);
|
|
249
|
+
|
|
250
|
+
openDeepLink(url);
|
|
251
|
+
|
|
252
|
+
const params = await waitForCallback(id, this._config.timeoutMs);
|
|
253
|
+
const parsed = parseCallbackParams(params) as SignCallbackParams;
|
|
254
|
+
|
|
255
|
+
if (parsed.status === 'rejected') {
|
|
256
|
+
throw new ZeroWalletUserRejectionError(parsed.error || 'User rejected signature.');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!parsed.signature) {
|
|
260
|
+
throw new ZeroWalletSignMessageError('No signature returned.');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return deserializeSignature(parsed.signature);
|
|
264
|
+
} catch (error: any) {
|
|
265
|
+
this.emit('error', error);
|
|
266
|
+
if (error instanceof ZeroWalletTimeoutError) {
|
|
267
|
+
throw new WalletTimeoutError(error?.message, error);
|
|
268
|
+
}
|
|
269
|
+
throw new WalletSignMessageError(error?.message, error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
package/src/deeplink.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ZeroWalletTimeoutError } from './errors';
|
|
2
|
+
import { ConnectCallbackParams, SignCallbackParams } from './types';
|
|
3
|
+
import { generateCallbackId, isMobileDevice } from './utils';
|
|
4
|
+
|
|
5
|
+
const ZERO_WALLET_SCHEME = 'zerowallet://';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds the URL to connect to the Zero Wallet mobile app deep link.
|
|
9
|
+
*/
|
|
10
|
+
export function buildConnectUrl(callbackUrl: string, id: string): string {
|
|
11
|
+
return `${ZERO_WALLET_SCHEME}connect?callback=${encodeURIComponent(callbackUrl)}&id=${id}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds the deep link URL to request signing a single transaction.
|
|
16
|
+
*/
|
|
17
|
+
export function buildSignTransactionUrl(serializedTxBase64: string, callbackUrl: string, id: string, network: string = 'mainnet-beta'): string {
|
|
18
|
+
return `${ZERO_WALLET_SCHEME}sign?tx=${encodeURIComponent(serializedTxBase64)}&callback=${encodeURIComponent(callbackUrl)}&id=${id}&network=${network}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Builds the deep link URL to request signing multiple transactions.
|
|
23
|
+
*/
|
|
24
|
+
export function buildSignAllUrl(serializedTxsBase64: string[], callbackUrl: string, id: string, network: string = 'mainnet-beta'): string {
|
|
25
|
+
const serializedPayload = JSON.stringify(serializedTxsBase64); // JSON array of base64 strings
|
|
26
|
+
return `${ZERO_WALLET_SCHEME}signAll?txs=${encodeURIComponent(serializedPayload)}&callback=${encodeURIComponent(callbackUrl)}&id=${id}&network=${network}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Builds the deep link URL to request signing a raw message.
|
|
31
|
+
*/
|
|
32
|
+
export function buildSignMessageUrl(messageBase64: string, callbackUrl: string, id: string): string {
|
|
33
|
+
return `${ZERO_WALLET_SCHEME}signMessage?msg=${encodeURIComponent(messageBase64)}&callback=${encodeURIComponent(callbackUrl)}&id=${id}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Attempts to open the deep link using window.location.href.
|
|
38
|
+
*/
|
|
39
|
+
export function openDeepLink(url: string): void {
|
|
40
|
+
if (typeof window !== 'undefined') {
|
|
41
|
+
window.location.href = url;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detects if the current user agent is mobile.
|
|
47
|
+
*/
|
|
48
|
+
export function isMobile(): boolean {
|
|
49
|
+
return isMobileDevice();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checks for the presence of the Zero Wallet browser extension (or injected provider wrapper if present).
|
|
54
|
+
* Currently always returns false conceptually via deep links, but standardizes the check.
|
|
55
|
+
*/
|
|
56
|
+
export function isZeroWalletInstalled(): boolean {
|
|
57
|
+
if (typeof window === 'undefined') return false;
|
|
58
|
+
return !!(window as any).zerowallet?.isZeroWallet;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Waits for a callback matching the provided request ID.
|
|
63
|
+
* Since redirect callbacks typically reload the page, if you are a SPA you must register this listener
|
|
64
|
+
* or catch it on boot in the adapter itself.
|
|
65
|
+
*
|
|
66
|
+
* For in-app browser or local state preservation, we listen to the hashchange/search params.
|
|
67
|
+
*/
|
|
68
|
+
export function waitForCallback(
|
|
69
|
+
expectedId: string,
|
|
70
|
+
timeoutMs: number = 120000
|
|
71
|
+
): Promise<URLSearchParams> {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
let timeoutId: NodeJS.Timeout;
|
|
74
|
+
|
|
75
|
+
// Note: For deep links, true "waiting" like this only works if the dApp is in a container
|
|
76
|
+
// that handles the URL return without reloading, or uses a specific communication bridge.
|
|
77
|
+
// For external browsers, the return callback will act as a page navigation to the dApp's callback URL.
|
|
78
|
+
const checkUrl = () => {
|
|
79
|
+
if (typeof window === 'undefined') return;
|
|
80
|
+
const params = new URLSearchParams(window.location.search);
|
|
81
|
+
if (params.get('id') === expectedId) {
|
|
82
|
+
clearTimeout(timeoutId);
|
|
83
|
+
clearInterval(intervalId);
|
|
84
|
+
resolve(params);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const intervalId = setInterval(checkUrl, 1000); // Check every second
|
|
89
|
+
checkUrl(); // check initially
|
|
90
|
+
|
|
91
|
+
timeoutId = setTimeout(() => {
|
|
92
|
+
clearInterval(intervalId);
|
|
93
|
+
reject(new ZeroWalletTimeoutError('Deep link response timed out.'));
|
|
94
|
+
}, timeoutMs);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parses callback URL SearchParams into typed parameters.
|
|
100
|
+
*/
|
|
101
|
+
export function parseCallbackParams(params: URLSearchParams): ConnectCallbackParams | SignCallbackParams {
|
|
102
|
+
const id = params.get('id') || '';
|
|
103
|
+
const status = (params.get('status') as 'approved' | 'rejected') || 'rejected';
|
|
104
|
+
const error = params.get('error') || undefined;
|
|
105
|
+
|
|
106
|
+
// Connect Params
|
|
107
|
+
const publicKey = params.get('publicKey') || undefined;
|
|
108
|
+
|
|
109
|
+
// Sign Params
|
|
110
|
+
const signedTx = params.get('signedTx') || undefined;
|
|
111
|
+
let signedTxs: string[] | undefined;
|
|
112
|
+
try {
|
|
113
|
+
const rawTxs = params.get('signedTxs');
|
|
114
|
+
if (rawTxs) {
|
|
115
|
+
signedTxs = JSON.parse(decodeURIComponent(rawTxs));
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error('Failed to parse signedTxs array', e);
|
|
119
|
+
}
|
|
120
|
+
const signature = params.get('signature') || undefined;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id,
|
|
124
|
+
status,
|
|
125
|
+
error,
|
|
126
|
+
publicKey,
|
|
127
|
+
signedTx,
|
|
128
|
+
signedTxs,
|
|
129
|
+
signature
|
|
130
|
+
};
|
|
131
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { WalletError } from '@solana/wallet-adapter-base';
|
|
2
|
+
|
|
3
|
+
export class ZeroWalletNotInstalledError extends WalletError {
|
|
4
|
+
name = 'ZeroWalletNotInstalledError';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ZeroWalletConnectionError extends WalletError {
|
|
8
|
+
name = 'ZeroWalletConnectionError';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ZeroWalletUserRejectionError extends WalletError {
|
|
12
|
+
name = 'ZeroWalletUserRejectionError';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ZeroWalletSignTransactionError extends WalletError {
|
|
16
|
+
name = 'ZeroWalletSignTransactionError';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ZeroWalletSignMessageError extends WalletError {
|
|
20
|
+
name = 'ZeroWalletSignMessageError';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ZeroWalletTimeoutError extends WalletError {
|
|
24
|
+
name = 'ZeroWalletTimeoutError';
|
|
25
|
+
}
|
package/src/index.ts
ADDED
package/src/standard.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { SolanaSignAndSendTransactionFeature, SolanaSignAndSendTransactionMethod, SolanaSignMessageFeature, SolanaSignMessageMethod, SolanaSignTransactionFeature, SolanaSignTransactionMethod } from '@solana/wallet-standard-features';
|
|
2
|
+
import { Wallet, WalletAccount, WalletIcon } from '@wallet-standard/core';
|
|
3
|
+
import { ZeroWalletAdapter, ZeroWalletIcon, ZeroWalletName } from './adapter';
|
|
4
|
+
import { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
|
|
5
|
+
import bs58 from 'bs58';
|
|
6
|
+
import { StandardConnectFeature, StandardConnectMethod, StandardDisconnectFeature, StandardDisconnectMethod, StandardEventsFeature, StandardEventsListeners, StandardEventsNames, StandardEventsOnMethod } from '@wallet-standard/features';
|
|
7
|
+
|
|
8
|
+
export interface ZeroWalletWindowStandard extends Window {
|
|
9
|
+
zerowallet?: {
|
|
10
|
+
isZeroWallet?: boolean;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ZeroWalletFeature = StandardConnectFeature &
|
|
15
|
+
StandardDisconnectFeature &
|
|
16
|
+
StandardEventsFeature &
|
|
17
|
+
SolanaSignTransactionFeature &
|
|
18
|
+
SolanaSignAndSendTransactionFeature &
|
|
19
|
+
SolanaSignMessageFeature;
|
|
20
|
+
|
|
21
|
+
export class ZeroWalletStandardWallet implements Wallet {
|
|
22
|
+
readonly #adapter: ZeroWalletAdapter;
|
|
23
|
+
readonly #listeners: { [E in StandardEventsNames]?: StandardEventsListeners[E][] } = {};
|
|
24
|
+
readonly #version = '1.0.0' as const;
|
|
25
|
+
|
|
26
|
+
get version() {
|
|
27
|
+
return this.#version;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get name() {
|
|
31
|
+
return ZeroWalletName;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get icon(): WalletIcon {
|
|
35
|
+
return ZeroWalletIcon as WalletIcon;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get chains() {
|
|
39
|
+
return ['solana:mainnet', 'solana:devnet', 'solana:testnet'] as const;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get features(): ZeroWalletFeature {
|
|
43
|
+
return {
|
|
44
|
+
'standard:connect': {
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
connect: this.#connect,
|
|
47
|
+
},
|
|
48
|
+
'standard:disconnect': {
|
|
49
|
+
version: '1.0.0',
|
|
50
|
+
disconnect: this.#disconnect,
|
|
51
|
+
},
|
|
52
|
+
'standard:events': {
|
|
53
|
+
version: '1.0.0',
|
|
54
|
+
on: this.#on,
|
|
55
|
+
},
|
|
56
|
+
'solana:signTransaction': {
|
|
57
|
+
version: '1.0.0',
|
|
58
|
+
supportedTransactionVersions: ['legacy', 0],
|
|
59
|
+
signTransaction: this.#signTransaction,
|
|
60
|
+
},
|
|
61
|
+
'solana:signAndSendTransaction': {
|
|
62
|
+
version: '1.0.0',
|
|
63
|
+
supportedTransactionVersions: ['legacy', 0],
|
|
64
|
+
signAndSendTransaction: this.#signAndSendTransaction,
|
|
65
|
+
},
|
|
66
|
+
'solana:signMessage': {
|
|
67
|
+
version: '1.0.0',
|
|
68
|
+
signMessage: this.#signMessage,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get accounts() {
|
|
74
|
+
return this.#adapter.publicKey ? [new ZeroWalletStandardAccount(this.#adapter.publicKey)] : [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
constructor(adapter: ZeroWalletAdapter) {
|
|
78
|
+
this.#adapter = adapter;
|
|
79
|
+
this.#adapter.on('connect', this.#emit.bind(this, 'change', { accounts: this.accounts }));
|
|
80
|
+
this.#adapter.on('disconnect', this.#emit.bind(this, 'change', { accounts: this.accounts }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#connect: StandardConnectMethod = async ({ silent } = {}) => {
|
|
84
|
+
if (!this.#adapter.connected) {
|
|
85
|
+
await this.#adapter.connect();
|
|
86
|
+
}
|
|
87
|
+
return { accounts: this.accounts };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
#disconnect: StandardDisconnectMethod = async () => {
|
|
91
|
+
await this.#adapter.disconnect();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
#on: StandardEventsOnMethod = (event, listener) => {
|
|
95
|
+
this.#listeners[event]?.push(listener) || (this.#listeners[event] = [listener]);
|
|
96
|
+
return (): void => this.#off(event, listener);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
#emit<E extends StandardEventsNames>(event: E, ...args: Parameters<StandardEventsListeners[E]>): void {
|
|
100
|
+
// eslint-disable-next-line prefer-spread
|
|
101
|
+
this.#listeners[event]?.forEach((listener) => listener.apply(null, args));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#off<E extends StandardEventsNames>(event: E, listener: StandardEventsListeners[E]): void {
|
|
105
|
+
this.#listeners[event] = this.#listeners[event]?.filter((existingListener) => listener !== existingListener);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#signTransaction: SolanaSignTransactionMethod = async (...inputs) => {
|
|
109
|
+
if (!this.#adapter.connected) throw new Error('Not connected');
|
|
110
|
+
const signedTxs = [];
|
|
111
|
+
for (const input of inputs) {
|
|
112
|
+
const tx = Transaction.from(input.transaction); // Using legacy for simplicity in standard adapter interface as fallback
|
|
113
|
+
const signed = await this.#adapter.signTransaction(tx);
|
|
114
|
+
signedTxs.push({ signedTransaction: signed.serialize() });
|
|
115
|
+
}
|
|
116
|
+
return signedTxs;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
#signAndSendTransaction: SolanaSignAndSendTransactionMethod = async (...inputs) => {
|
|
120
|
+
if (!this.#adapter.connected) throw new Error('Not connected');
|
|
121
|
+
throw new Error('signAndSendTransaction not fully implemented in standard adapter wrapper for Zero Wallet deep link yet.');
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
#signMessage: SolanaSignMessageMethod = async (...inputs) => {
|
|
125
|
+
if (!this.#adapter.connected) throw new Error('Not connected');
|
|
126
|
+
const signatures = [];
|
|
127
|
+
for (const input of inputs) {
|
|
128
|
+
const signature = await this.#adapter.signMessage(input.message);
|
|
129
|
+
signatures.push({ signedMessage: input.message, signature });
|
|
130
|
+
}
|
|
131
|
+
return signatures;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class ZeroWalletStandardAccount implements WalletAccount {
|
|
136
|
+
readonly #publicKey: PublicKey;
|
|
137
|
+
|
|
138
|
+
get address() {
|
|
139
|
+
return this.#publicKey.toBase58();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get publicKey() {
|
|
143
|
+
return this.#publicKey.toBytes();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get chains() {
|
|
147
|
+
return ['solana:mainnet', 'solana:devnet', 'solana:testnet'] as const;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get features() {
|
|
151
|
+
return ['solana:signTransaction', 'solana:signAndSendTransaction', 'solana:signMessage'] as const;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
constructor(publicKey: PublicKey) {
|
|
155
|
+
this.#publicKey = publicKey;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Registers the ZeroWallet in the Wallet Standard modal ecosystem.
|
|
161
|
+
*/
|
|
162
|
+
export function registerWalletStandard(adapter: ZeroWalletAdapter): void {
|
|
163
|
+
if (typeof window === 'undefined') return;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const wallet = new ZeroWalletStandardWallet(adapter);
|
|
167
|
+
const { registerWallet } = require('@wallet-standard/wallet') as typeof import('@wallet-standard/wallet');
|
|
168
|
+
registerWallet(wallet);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('ZeroWallet: Failed to register wallet standard', error);
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface ZeroWalletWindow extends Window {
|
|
2
|
+
zerowallet?: {
|
|
3
|
+
isZeroWallet?: boolean;
|
|
4
|
+
};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ConnectResponse {
|
|
8
|
+
publicKey: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SignTransactionResponse {
|
|
12
|
+
signedTransaction: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SignAllTransactionsResponse {
|
|
16
|
+
signedTransactions: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SignMessageResponse {
|
|
20
|
+
signature: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Params attached to the callback URL by the mobile app
|
|
24
|
+
export interface DeepLinkCallbackParams {
|
|
25
|
+
id: string;
|
|
26
|
+
status: 'approved' | 'rejected';
|
|
27
|
+
error?: string; // Set when status is rejected
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ConnectCallbackParams extends DeepLinkCallbackParams {
|
|
31
|
+
publicKey?: string; // Base58 encoded pulic key string (on success)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SignCallbackParams extends DeepLinkCallbackParams {
|
|
35
|
+
signedTx?: string; // Base64 encoded signed transaction
|
|
36
|
+
signedTxs?: string[]; // Array of base64 encoded signed transactions
|
|
37
|
+
signature?: string; // Base64 encoded signature
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ZeroWalletAdapterConfig {
|
|
41
|
+
/**
|
|
42
|
+
* Network cluster.
|
|
43
|
+
*/
|
|
44
|
+
network?: 'mainnet-beta' | 'devnet' | 'testnet';
|
|
45
|
+
/**
|
|
46
|
+
* Timeout for deep link callbacks in milliseconds. Defaults to 120000 (2 minutes).
|
|
47
|
+
*/
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* The callback scheme/host your dApp is listening on. e.g., "my-dapp://callback"
|
|
51
|
+
*/
|
|
52
|
+
callbackUrl?: string;
|
|
53
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Transaction, VersionedTransaction } from '@solana/web3.js';
|
|
2
|
+
import bs58 from 'bs58';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Serializes a transaction (legacy or versioned) to a base64 encoded string.
|
|
6
|
+
*/
|
|
7
|
+
export function serializeTransaction(tx: Transaction | VersionedTransaction): string {
|
|
8
|
+
if ('version' in tx) {
|
|
9
|
+
// Versioned transaction
|
|
10
|
+
return encodeBase64(tx.serialize());
|
|
11
|
+
} else {
|
|
12
|
+
// Legacy transaction.
|
|
13
|
+
// Needs recentBlockhash and feePayer before serialize, but typically dApp provides this
|
|
14
|
+
// Use requireAllSignatures: false if the wallet still needs to add signatures
|
|
15
|
+
return encodeBase64(tx.serialize({ requireAllSignatures: false }));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deserializes a base64 encoded transaction back into a Transaction or VersionedTransaction.
|
|
21
|
+
*/
|
|
22
|
+
export function deserializeTransaction(serializedTxBase64: string): Transaction | VersionedTransaction {
|
|
23
|
+
const buffer = decodeBase64(serializedTxBase64);
|
|
24
|
+
try {
|
|
25
|
+
// Attempt versioned transaction parsing
|
|
26
|
+
return VersionedTransaction.deserialize(buffer);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Fallback to legacy
|
|
29
|
+
return Transaction.from(buffer);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Serializes a raw message Uint8Array to base64.
|
|
35
|
+
*/
|
|
36
|
+
export function serializeMessage(message: Uint8Array): string {
|
|
37
|
+
return encodeBase64(message);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Deserializes a base64 signature back to Uint8Array.
|
|
42
|
+
*/
|
|
43
|
+
export function deserializeSignature(signatureBase64: string): Uint8Array {
|
|
44
|
+
return decodeBase64(signatureBase64);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Converts a Uint8Array to a base64 string.
|
|
49
|
+
*/
|
|
50
|
+
export function encodeBase64(data: Uint8Array): string {
|
|
51
|
+
if (typeof btoa !== 'undefined') {
|
|
52
|
+
const binString = Array.from(data, (byte) => String.fromCharCode(byte)).join('');
|
|
53
|
+
return btoa(binString);
|
|
54
|
+
}
|
|
55
|
+
return Buffer.from(data).toString('base64');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Converts a base64 string to a Uint8Array.
|
|
60
|
+
*/
|
|
61
|
+
export function decodeBase64(data: string): Uint8Array {
|
|
62
|
+
if (typeof atob !== 'undefined') {
|
|
63
|
+
const binString = atob(data);
|
|
64
|
+
return new Uint8Array(binString.split('').map((char) => char.charCodeAt(0)));
|
|
65
|
+
}
|
|
66
|
+
return new Uint8Array(Buffer.from(data, 'base64'));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generates a unique 16 character hex string for tracking deep link requests.
|
|
71
|
+
*/
|
|
72
|
+
export function generateCallbackId(): string {
|
|
73
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Detects if the current environment is a mobile device via User-Agent.
|
|
78
|
+
*/
|
|
79
|
+
export function isMobileDevice(): boolean {
|
|
80
|
+
if (typeof navigator === 'undefined' || !navigator.userAgent) return false;
|
|
81
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isIOS(): boolean {
|
|
85
|
+
if (typeof navigator === 'undefined' || !navigator.userAgent) return false;
|
|
86
|
+
return /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isAndroid(): boolean {
|
|
90
|
+
if (typeof navigator === 'undefined' || !navigator.userAgent) return false;
|
|
91
|
+
return /Android/i.test(navigator.userAgent);
|
|
92
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { ZeroWalletAdapter } from '../src/adapter';
|
|
2
|
+
import { WalletReadyState, WalletTimeoutError, WalletDisconnectedError } from '@solana/wallet-adapter-base';
|
|
3
|
+
import * as deeplink from '../src/deeplink';
|
|
4
|
+
import { ZeroWalletTimeoutError, ZeroWalletUserRejectionError, ZeroWalletNotInstalledError } from '../src/errors';
|
|
5
|
+
import { Transaction, PublicKey, SystemProgram } from '@solana/web3.js';
|
|
6
|
+
|
|
7
|
+
// Mock the openDeepLink function
|
|
8
|
+
jest.mock('../src/deeplink', () => ({
|
|
9
|
+
...jest.requireActual('../src/deeplink'),
|
|
10
|
+
openDeepLink: jest.fn(),
|
|
11
|
+
waitForCallback: jest.fn(),
|
|
12
|
+
isMobile: jest.fn(),
|
|
13
|
+
isZeroWalletInstalled: jest.fn()
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('ZeroWalletAdapter', () => {
|
|
17
|
+
let adapter: ZeroWalletAdapter;
|
|
18
|
+
const mockCallbackUrl = 'https://tester.com/callback';
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Reset mocks
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
|
|
24
|
+
// Default to mobile environment for testing deep links
|
|
25
|
+
(deeplink.isMobile as jest.Mock).mockReturnValue(true);
|
|
26
|
+
(deeplink.isZeroWalletInstalled as jest.Mock).mockReturnValue(true);
|
|
27
|
+
|
|
28
|
+
adapter = new ZeroWalletAdapter({ callbackUrl: mockCallbackUrl, timeoutMs: 1000 });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('readyState', () => {
|
|
32
|
+
it('returns Installed on mobile if zero wallet is detected', () => {
|
|
33
|
+
expect(adapter.readyState).toBe(WalletReadyState.Installed);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns Loadable on desktop if not explicitly installed', () => {
|
|
37
|
+
(deeplink.isMobile as jest.Mock).mockReturnValue(false);
|
|
38
|
+
(deeplink.isZeroWalletInstalled as jest.Mock).mockReturnValue(false);
|
|
39
|
+
|
|
40
|
+
// Re-instantiate to pickup mocked values
|
|
41
|
+
adapter = new ZeroWalletAdapter();
|
|
42
|
+
expect(adapter.readyState).toBe(WalletReadyState.Loadable);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('connect()', () => {
|
|
47
|
+
it('opens deep link and resolves with public key on approval', async () => {
|
|
48
|
+
const pubKeyString = '11111111111111111111111111111111';
|
|
49
|
+
|
|
50
|
+
// Mock waiting for callback returning approved status
|
|
51
|
+
const params = new URLSearchParams(`?id=mockid&status=approved&publicKey=${pubKeyString}`);
|
|
52
|
+
(deeplink.waitForCallback as jest.Mock).mockResolvedValueOnce(params);
|
|
53
|
+
|
|
54
|
+
const connectPromise = adapter.connect();
|
|
55
|
+
|
|
56
|
+
expect(adapter.connecting).toBe(true);
|
|
57
|
+
expect(deeplink.openDeepLink).toHaveBeenCalled();
|
|
58
|
+
|
|
59
|
+
await connectPromise;
|
|
60
|
+
|
|
61
|
+
expect(adapter.connecting).toBe(false);
|
|
62
|
+
expect(adapter.connected).toBe(true);
|
|
63
|
+
expect(adapter.publicKey?.toBase58()).toBe(pubKeyString);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('rejects when user cancels', async () => {
|
|
67
|
+
const params = new URLSearchParams(`?id=mockid&status=rejected&error=User%20cancelled`);
|
|
68
|
+
(deeplink.waitForCallback as jest.Mock).mockResolvedValueOnce(params);
|
|
69
|
+
|
|
70
|
+
await expect(adapter.connect()).rejects.toThrow(ZeroWalletUserRejectionError);
|
|
71
|
+
expect(adapter.connected).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('throws WalletTimeoutError on timeout', async () => {
|
|
75
|
+
(deeplink.waitForCallback as jest.Mock).mockRejectedValueOnce(new ZeroWalletTimeoutError());
|
|
76
|
+
|
|
77
|
+
await expect(adapter.connect()).rejects.toThrow(WalletTimeoutError);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('signTransaction()', () => {
|
|
82
|
+
let tx: Transaction;
|
|
83
|
+
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
// Must be connected to sign
|
|
86
|
+
(deeplink.waitForCallback as jest.Mock).mockResolvedValueOnce(
|
|
87
|
+
new URLSearchParams(`?status=approved&publicKey=11111111111111111111111111111111`)
|
|
88
|
+
);
|
|
89
|
+
await adapter.connect();
|
|
90
|
+
|
|
91
|
+
tx = new Transaction();
|
|
92
|
+
tx.add(SystemProgram.transfer({
|
|
93
|
+
fromPubkey: adapter.publicKey!,
|
|
94
|
+
toPubkey: new PublicKey('22222222222222222222222222222222'),
|
|
95
|
+
lamports: 1000,
|
|
96
|
+
}));
|
|
97
|
+
tx.recentBlockhash = '33333333333333333333333333333333';
|
|
98
|
+
tx.feePayer = adapter.publicKey!;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('serializes tx, sends deep link, returns signed tx', async () => {
|
|
102
|
+
// Mock signing response
|
|
103
|
+
const signedSerialized = (() => {
|
|
104
|
+
const signedTx = new Transaction();
|
|
105
|
+
// Just use same tx for mocking
|
|
106
|
+
signedTx.add(SystemProgram.transfer({
|
|
107
|
+
fromPubkey: adapter.publicKey!,
|
|
108
|
+
toPubkey: new PublicKey('22222222222222222222222222222222'),
|
|
109
|
+
lamports: 1000,
|
|
110
|
+
}));
|
|
111
|
+
signedTx.recentBlockhash = '33333333333333333333333333333333';
|
|
112
|
+
signedTx.feePayer = adapter.publicKey!;
|
|
113
|
+
// Requires signatures array fake
|
|
114
|
+
signedTx.signatures.push({ publicKey: adapter.publicKey!, signature: Buffer.alloc(64) });
|
|
115
|
+
return signedTx.serialize({ requireAllSignatures: false, verifySignatures: false }).toString('base64');
|
|
116
|
+
})();
|
|
117
|
+
|
|
118
|
+
const params = new URLSearchParams(`?status=approved&signedTx=${encodeURIComponent(signedSerialized)}`);
|
|
119
|
+
(deeplink.waitForCallback as jest.Mock).mockResolvedValueOnce(params);
|
|
120
|
+
|
|
121
|
+
const result = await adapter.signTransaction(tx);
|
|
122
|
+
|
|
123
|
+
expect(result).toBeInstanceOf(Transaction);
|
|
124
|
+
expect(deeplink.openDeepLink).toHaveBeenCalledTimes(2); // once for connect, once for sign
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('disconnect()', () => {
|
|
129
|
+
it('clears state and emits event', async () => {
|
|
130
|
+
(deeplink.waitForCallback as jest.Mock).mockResolvedValueOnce(
|
|
131
|
+
new URLSearchParams(`?status=approved&publicKey=11111111111111111111111111111111`)
|
|
132
|
+
);
|
|
133
|
+
await adapter.connect();
|
|
134
|
+
expect(adapter.connected).toBe(true);
|
|
135
|
+
|
|
136
|
+
await adapter.disconnect();
|
|
137
|
+
|
|
138
|
+
expect(adapter.connected).toBe(false);
|
|
139
|
+
expect(adapter.publicKey).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildConnectUrl,
|
|
3
|
+
buildSignTransactionUrl,
|
|
4
|
+
buildSignAllUrl,
|
|
5
|
+
buildSignMessageUrl,
|
|
6
|
+
waitForCallback,
|
|
7
|
+
isMobile,
|
|
8
|
+
parseCallbackParams
|
|
9
|
+
} from '../src/deeplink';
|
|
10
|
+
import { ZeroWalletTimeoutError } from '../src/errors';
|
|
11
|
+
|
|
12
|
+
describe('deeplink', () => {
|
|
13
|
+
const callbackUrl = 'https://my-dapp.com/callback';
|
|
14
|
+
const id = '1234567890abcdef';
|
|
15
|
+
|
|
16
|
+
describe('URL Builders', () => {
|
|
17
|
+
it('buildConnectUrl returns correct URL format', () => {
|
|
18
|
+
const url = buildConnectUrl(callbackUrl, id);
|
|
19
|
+
expect(url).toBe(`zerowallet://connect?callback=https%3A%2F%2Fmy-dapp.com%2Fcallback&id=${id}`);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('buildSignTransactionUrl encodes transaction correctly', () => {
|
|
23
|
+
const txBase64 = 'aGVsbG8='; // hello
|
|
24
|
+
const url = buildSignTransactionUrl(txBase64, callbackUrl, id, 'devnet');
|
|
25
|
+
expect(url).toBe(`zerowallet://sign?tx=aGVsbG8%3D&callback=https%3A%2F%2Fmy-dapp.com%2Fcallback&id=${id}&network=devnet`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('buildSignAllUrl encodes array correctly', () => {
|
|
29
|
+
const txs = ['tx1', 'tx2'];
|
|
30
|
+
const url = buildSignAllUrl(txs, callbackUrl, id);
|
|
31
|
+
const expectedPayload = encodeURIComponent(JSON.stringify(txs));
|
|
32
|
+
expect(url).toBe(`zerowallet://signAll?txs=${expectedPayload}&callback=https%3A%2F%2Fmy-dapp.com%2Fcallback&id=${id}&network=mainnet-beta`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('buildSignMessageUrl encodes message correctly', () => {
|
|
36
|
+
const url = buildSignMessageUrl('bXNn', callbackUrl, id);
|
|
37
|
+
expect(url).toBe(`zerowallet://signMessage?msg=bXNn&callback=https%3A%2F%2Fmy-dapp.com%2Fcallback&id=${id}`);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('waitForCallback', () => {
|
|
42
|
+
let originalWindow: any;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
jest.useFakeTimers();
|
|
46
|
+
originalWindow = global.window;
|
|
47
|
+
global.window = {
|
|
48
|
+
location: {
|
|
49
|
+
search: ''
|
|
50
|
+
}
|
|
51
|
+
} as any;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
jest.useRealTimers();
|
|
56
|
+
global.window = originalWindow;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('resolves when callback received', async () => {
|
|
60
|
+
const waitPromise = waitForCallback(id, 5000);
|
|
61
|
+
|
|
62
|
+
// Advance time a bit and set the window search
|
|
63
|
+
jest.advanceTimersByTime(1500);
|
|
64
|
+
global.window.location.search = `?id=${id}&status=approved`;
|
|
65
|
+
|
|
66
|
+
// trigger next check
|
|
67
|
+
jest.advanceTimersByTime(1000);
|
|
68
|
+
|
|
69
|
+
const params = await waitPromise;
|
|
70
|
+
expect(params.get('id')).toBe(id);
|
|
71
|
+
expect(params.get('status')).toBe('approved');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('rejects on timeout', async () => {
|
|
75
|
+
const waitPromise = waitForCallback(id, 2000);
|
|
76
|
+
|
|
77
|
+
// Advance time past timeout without ever setting window search
|
|
78
|
+
jest.advanceTimersByTime(2500);
|
|
79
|
+
|
|
80
|
+
await expect(waitPromise).rejects.toThrow(ZeroWalletTimeoutError);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('parseCallbackParams', () => {
|
|
85
|
+
it('parses connect approved', () => {
|
|
86
|
+
const params = new URLSearchParams(`?id=${id}&status=approved&publicKey=1111`);
|
|
87
|
+
const parsed = parseCallbackParams(params);
|
|
88
|
+
|
|
89
|
+
expect(parsed.id).toBe(id);
|
|
90
|
+
expect(parsed.status).toBe('approved');
|
|
91
|
+
expect(parsed.publicKey).toBe('1111');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('parses rejection', () => {
|
|
95
|
+
const params = new URLSearchParams(`?id=${id}&status=rejected&error=User%20cancelled`);
|
|
96
|
+
const parsed = parseCallbackParams(params);
|
|
97
|
+
|
|
98
|
+
expect(parsed.id).toBe(id);
|
|
99
|
+
expect(parsed.status).toBe('rejected');
|
|
100
|
+
expect(parsed.error).toBe('User cancelled');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { serializeTransaction, deserializeTransaction, encodeBase64, decodeBase64, isMobileDevice, isIOS, isAndroid } from '../src/utils';
|
|
2
|
+
import { Transaction, PublicKey, SystemProgram } from '@solana/web3.js';
|
|
3
|
+
import bs58 from 'bs58';
|
|
4
|
+
|
|
5
|
+
describe('utils', () => {
|
|
6
|
+
|
|
7
|
+
describe('Base64 Encoding/Decoding', () => {
|
|
8
|
+
it('encodeBase64 and decodeBase64 are inverse operations', () => {
|
|
9
|
+
const originalData = new Uint8Array([1, 2, 3, 255, 128, 0, 42]);
|
|
10
|
+
const encoded = encodeBase64(originalData);
|
|
11
|
+
expect(typeof encoded).toBe('string');
|
|
12
|
+
|
|
13
|
+
const decoded = decodeBase64(encoded);
|
|
14
|
+
expect(decoded).toEqual(originalData);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('handles strings properly', () => {
|
|
18
|
+
const str = 'hello world';
|
|
19
|
+
const bytes = new Uint8Array(Buffer.from(str));
|
|
20
|
+
const encoded = encodeBase64(bytes);
|
|
21
|
+
expect(encoded).toBe('aGVsbG8gd29ybGQ=');
|
|
22
|
+
|
|
23
|
+
const decoded = decodeBase64(encoded);
|
|
24
|
+
expect(Buffer.from(decoded).toString()).toBe(str);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('Transaction Serialization', () => {
|
|
29
|
+
it('serializeTransaction generates a base64 string for legacy tx', () => {
|
|
30
|
+
const tx = new Transaction();
|
|
31
|
+
tx.add(
|
|
32
|
+
SystemProgram.transfer({
|
|
33
|
+
fromPubkey: new PublicKey('11111111111111111111111111111111'),
|
|
34
|
+
toPubkey: new PublicKey('22222222222222222222222222222222'),
|
|
35
|
+
lamports: 1000,
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
tx.recentBlockhash = '11111111111111111111111111111111';
|
|
39
|
+
tx.feePayer = new PublicKey('11111111111111111111111111111111');
|
|
40
|
+
|
|
41
|
+
const serialized = serializeTransaction(tx);
|
|
42
|
+
expect(typeof serialized).toBe('string');
|
|
43
|
+
expect(serialized.length).toBeGreaterThan(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('deserializeTransaction round-trips correctly for legacy tx', () => {
|
|
47
|
+
const tx = new Transaction();
|
|
48
|
+
tx.add(
|
|
49
|
+
SystemProgram.transfer({
|
|
50
|
+
fromPubkey: new PublicKey('11111111111111111111111111111111'),
|
|
51
|
+
toPubkey: new PublicKey('22222222222222222222222222222222'),
|
|
52
|
+
lamports: 5000,
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
tx.recentBlockhash = '11111111111111111111111111111111';
|
|
56
|
+
tx.feePayer = new PublicKey('11111111111111111111111111111111');
|
|
57
|
+
|
|
58
|
+
const serialized = serializeTransaction(tx);
|
|
59
|
+
const deserialized = deserializeTransaction(serialized) as Transaction;
|
|
60
|
+
|
|
61
|
+
expect(deserialized).toBeInstanceOf(Transaction);
|
|
62
|
+
expect(deserialized.feePayer?.toBase58()).toBe(tx.feePayer?.toBase58());
|
|
63
|
+
expect(deserialized.recentBlockhash).toBe(tx.recentBlockhash);
|
|
64
|
+
expect(deserialized.instructions.length).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2020",
|
|
7
|
+
"DOM",
|
|
8
|
+
"DOM.Iterable"
|
|
9
|
+
],
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": true,
|
|
12
|
+
"moduleResolution": "node",
|
|
13
|
+
"strict": true,
|
|
14
|
+
"esModuleInterop": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"isolatedModules": true
|
|
19
|
+
},
|
|
20
|
+
"include": [
|
|
21
|
+
"src/**/*"
|
|
22
|
+
],
|
|
23
|
+
"exclude": [
|
|
24
|
+
"node_modules",
|
|
25
|
+
"dist",
|
|
26
|
+
"tests"
|
|
27
|
+
]
|
|
28
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['cjs', 'esm'],
|
|
6
|
+
dts: true,
|
|
7
|
+
splitting: false,
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
clean: true,
|
|
10
|
+
treeshake: true,
|
|
11
|
+
minify: process.env.NODE_ENV === 'production',
|
|
12
|
+
external: [
|
|
13
|
+
'@solana/wallet-adapter-base',
|
|
14
|
+
'@solana/web3.js',
|
|
15
|
+
'@solana/wallet-standard-features',
|
|
16
|
+
'@wallet-standard/core',
|
|
17
|
+
'@wallet-standard/features',
|
|
18
|
+
'@wallet-standard/wallet'
|
|
19
|
+
],
|
|
20
|
+
});
|